dqy08 commited on
Commit
82b33f3
·
1 Parent(s): 494c9e4

改进首页布局,增加视频预览;简化demo清单逻辑。

Browse files
.gitattributes CHANGED
@@ -10,6 +10,7 @@
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
  *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
 
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
  *.npy filter=lfs diff=lfs merge=lfs -text
15
  *.npz filter=lfs diff=lfs merge=lfs -text
 
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
  *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.mov filter=lfs diff=lfs merge=lfs -text
14
  *.msgpack filter=lfs diff=lfs merge=lfs -text
15
  *.npy filter=lfs diff=lfs merge=lfs -text
16
  *.npz filter=lfs diff=lfs merge=lfs -text
backend/access_log.py CHANGED
@@ -1,6 +1,7 @@
1
  """服务访问日志"""
2
  from datetime import datetime
3
  from typing import Optional
 
4
 
5
  from flask import request
6
  import threading
@@ -45,15 +46,12 @@ def _log_request(event_type: str, details: str = "", client_ip: str = None):
45
 
46
 
47
  def log_page_load(path: str):
48
- """记录页面访问(含 ?ref= 参数)"""
49
- details = f"path='{path}'"
50
  try:
51
- ref = request.args.get("ref")
52
- if ref:
53
- details += f", ref='{ref}'"
54
  except RuntimeError:
55
- pass
56
- _log_request("📄 页面访问", details)
57
 
58
 
59
  def log_demo_file(path: str):
 
1
  """服务访问日志"""
2
  from datetime import datetime
3
  from typing import Optional
4
+ from urllib.parse import unquote
5
 
6
  from flask import request
7
  import threading
 
46
 
47
 
48
  def log_page_load(path: str):
 
 
49
  try:
50
+ qs = request.query_string.decode("utf-8", errors="replace")
51
+ combined = f"{path}?{unquote(qs)}" if qs else path
 
52
  except RuntimeError:
53
+ combined = path
54
+ _log_request("📄 页面访问", f"path={combined!r}")
55
 
56
 
57
  def log_demo_file(path: str):
backend/api/static.py CHANGED
@@ -41,6 +41,8 @@ def register_static_routes(app):
41
  """serves all files from ./client/dist/ to ``/client/<path:path>``"""
42
  if path.endswith('.html'):
43
  log_page_load(path)
 
 
44
  return _read_static_file('client/dist', path)
45
 
46
  @app.route('/demo/<path:path>')
 
41
  """serves all files from ./client/dist/ to ``/client/<path:path>``"""
42
  if path.endswith('.html'):
43
  log_page_load(path)
44
+ if path.endswith('.json'):
45
+ log_demo_file(path)
46
  return _read_static_file('client/dist', path)
47
 
48
  @app.route('/demo/<path:path>')
client/src/analysis.html CHANGED
@@ -18,7 +18,7 @@
18
  <header class="app-page-toolbar app-page-toolbar--bleed">
19
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
20
  <div class="app-page-toolbar-actions">
21
- <a href="index.html" class="home-link" title="InfoLens Home" data-i18n="text,title">InfoLens Home</a>
22
  <a href="compare.html?showTextRender=1&demos=/quick-start-1.json,/quick-start-2.json" target="_blank" class="compare-link" style="display: none;" title="Compare analysis results" data-i18n="text,title">Compare results</a>
23
  <div class="settings-menu-wrapper">
24
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
 
18
  <header class="app-page-toolbar app-page-toolbar--bleed">
19
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
20
  <div class="app-page-toolbar-actions">
21
+ <a href="index.html" class="home-link" title="Info Lens" data-i18n="text,title">Info Lens</a>
22
  <a href="compare.html?showTextRender=1&demos=/quick-start-1.json,/quick-start-2.json" target="_blank" class="compare-link" style="display: none;" title="Compare analysis results" data-i18n="text,title">Compare results</a>
23
  <div class="settings-menu-wrapper">
24
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
client/src/attribution.html CHANGED
@@ -16,7 +16,7 @@
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
- <a href="index.html" class="home-link" title="InfoLens Home" data-i18n="text,title">InfoLens Home</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
 
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
+ <a href="index.html" class="home-link" title="Info Lens" data-i18n="text,title">Info Lens</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
client/src/chat.html CHANGED
@@ -16,7 +16,7 @@
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
- <a href="index.html" class="home-link" title="InfoLens Home" data-i18n="text,title">InfoLens Home</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
 
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
+ <a href="index.html" class="home-link" title="Info Lens" data-i18n="text,title">Info Lens</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
client/src/content/images/attribute-dark.png CHANGED

Git LFS Details

  • SHA256: 5bfb1953f9e589e42663a72254d9cd6b461852596524928f630a88bb97746c4d
  • Pointer size: 130 Bytes
  • Size of remote file: 88 kB

Git LFS Details

  • SHA256: cb92cb63b4e25aa47ddaa1d045de206de91f57b7e577260dc8757faf5723d49d
  • Pointer size: 130 Bytes
  • Size of remote file: 47.4 kB
client/src/content/images/attribute.png CHANGED

Git LFS Details

  • SHA256: fc87ac09ae4aed3732f6abf90b92a439c553e953530bcc7d0f4793522f8eff55
  • Pointer size: 130 Bytes
  • Size of remote file: 91.8 kB

Git LFS Details

  • SHA256: 236a37b458b4758c4f4c1c0b48763af30c519b6440c145131f210653d49d7a1b
  • Pointer size: 130 Bytes
  • Size of remote file: 48.3 kB
client/src/content/images/chat-dark.png CHANGED

Git LFS Details

  • SHA256: 22a26beaaebb92d551ff6beb7ace1b2c6a1de802f811a93cc60bf7ba5620998c
  • Pointer size: 130 Bytes
  • Size of remote file: 52 kB

Git LFS Details

  • SHA256: 4db54951955c294189350d80d03e900469bcb045534069db83a3fa251d490ddf
  • Pointer size: 130 Bytes
  • Size of remote file: 15.4 kB
client/src/content/images/chat.png CHANGED

Git LFS Details

  • SHA256: f6c058cd261f6e42dd84f1d76c94bf5d72a1d898f5c5c976a157782d39f4bd86
  • Pointer size: 130 Bytes
  • Size of remote file: 55.9 kB

Git LFS Details

  • SHA256: f562a2e71547069a004443f0a70c1f29d61a02ef4b358ae78a48d8068de51e43
  • Pointer size: 130 Bytes
  • Size of remote file: 16.9 kB
client/src/content/images/dag-cn.png CHANGED

Git LFS Details

  • SHA256: 839129a3d1648ac454556a8546c1eda5de004dea3fe46bdc387e798451ddf115
  • Pointer size: 131 Bytes
  • Size of remote file: 167 kB

Git LFS Details

  • SHA256: bd52cca0851a5713e2e79d3ff4de0e36059018053074c3387def4e7401e9bd6a
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
client/src/content/images/dag-dark-cn.png CHANGED

Git LFS Details

  • SHA256: c8171562c134f3a5c0c4a5e32c77b373e8f68e1414489a21e766e9218e59391d
  • Pointer size: 131 Bytes
  • Size of remote file: 174 kB

Git LFS Details

  • SHA256: 6db1da30cee4f020306db19440fe25251cddb4d585c5039a09f0146a70db8747
  • Pointer size: 131 Bytes
  • Size of remote file: 122 kB
client/src/content/images/dag-dark-en.png CHANGED

Git LFS Details

  • SHA256: 4cd267bf100289ecb618ce6c8f6efae0ca4e46e013787503b8748aed8201a1bd
  • Pointer size: 131 Bytes
  • Size of remote file: 366 kB

Git LFS Details

  • SHA256: 4692f6096578d1258124d6b8af113922072537777ab74bed46b169d6a7a00351
  • Pointer size: 131 Bytes
  • Size of remote file: 266 kB
client/src/content/images/dag-dark.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed07cfaa00a91cfc7ed3ddc0edfa19747e864127053af2bf9bce24dcdfa39c65
3
+ size 132450
client/src/content/images/dag-en.png CHANGED

Git LFS Details

  • SHA256: 1301199b64cb4672227e7033d31e67339db1d28ae0def4074a93db1f5a91f949
  • Pointer size: 131 Bytes
  • Size of remote file: 401 kB

Git LFS Details

  • SHA256: 2042bb6f9c083bf9e8b11615fc435e66fcceef44185a878df82821f6f88001a9
  • Pointer size: 131 Bytes
  • Size of remote file: 301 kB
client/src/content/images/dag.mov ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:67b81278d3c4a0e311bd4c7d2ebea552e91b1a0b4fbcc3f85568490a0098d768
3
+ size 164323
client/src/content/images/highlight-dark.png CHANGED

Git LFS Details

  • SHA256: 9ca1f1ada601ee5ad7a26f36930c326632ab3a937c05a22d0ded5ae732781c72
  • Pointer size: 130 Bytes
  • Size of remote file: 76.8 kB

Git LFS Details

  • SHA256: 1d90ca98855edc2d9e3f59bfa2dc24e46483e5eef59167f8f4e9a45ff54d1a0c
  • Pointer size: 130 Bytes
  • Size of remote file: 40.5 kB
client/src/content/images/highlight.png CHANGED

Git LFS Details

  • SHA256: 1e70323305a191f5944bbda4d95997df6ff16defdb363e3f9e3a774b60d327d2
  • Pointer size: 130 Bytes
  • Size of remote file: 78.1 kB

Git LFS Details

  • SHA256: 6661fed1cae4ad36eb0c1b79d973af0a215ce3166ce71235f625b86697a64277
  • Pointer size: 130 Bytes
  • Size of remote file: 49.1 kB
client/src/css/home.scss CHANGED
@@ -1,6 +1,23 @@
1
  $breakpoint-mobile: 767px;
2
 
3
- // 导航页主/副标题字阶与顶栏子页解耦;仅本页使用下列变量
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  body.nav-landing-page {
5
  --nav-landing-title-size: 22px;
6
  --nav-landing-title-weight: 600;
@@ -11,8 +28,6 @@ body.nav-landing-page {
11
  --nav-landing-card-bg: #eeeeee;
12
  --nav-landing-card-bg-hover: #e0e0e0;
13
 
14
- // 覆盖子页/响应式里对 body{height:100vh} 的设定:固定视口高 + flex 纵向居中时,
15
- // 内容总高超过一屏会向上溢出,顶栏(设置按钮)会落在首屏外。
16
  position: relative;
17
  min-height: 100vh;
18
  height: auto;
@@ -25,7 +40,6 @@ body.nav-landing-page {
25
  background: var(--app-bg, #f5f5f5);
26
  }
27
 
28
- // 相对 body 右上角定位,随页面滚动;不参与 flex 流,不单独占一行
29
  .nav-landing-settings {
30
  position: absolute;
31
  top: max(12px, env(safe-area-inset-top));
@@ -43,13 +57,11 @@ html[data-theme='dark'] body.nav-landing-page {
43
 
44
  .nav-landing-main {
45
  width: 100%;
46
- // 与双列断点一致:窄/中屏仍由 width:100% 撑满视口(受 padding 限制)
47
  max-width: 44rem;
48
  box-sizing: border-box;
49
  padding: 2rem 1.25rem;
50
  }
51
 
52
- // 宽屏:放大主区域,卡片与预览块随之变宽变高
53
  @media (min-width: $breakpoint-mobile + 1) {
54
  .nav-landing-main {
55
  max-width: 64rem;
@@ -86,30 +98,92 @@ html[data-theme='dark'] body.nav-landing-page {
86
  letter-spacing: 0.02em;
87
  }
88
 
 
89
  .nav-landing-grid {
90
  display: grid;
91
- grid-template-columns: repeat(2, minmax(0, 1fr));
92
- gap: 0.75rem;
 
 
 
 
 
 
93
  }
94
 
95
- @media (max-width: $breakpoint-mobile) {
 
96
  .nav-landing-grid {
97
- grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
  }
100
 
 
101
  .nav-landing-card {
102
  display: flex;
103
  flex-direction: column;
104
- gap: 0.65rem;
105
- padding: 0.75rem 0.85rem;
106
- border-radius: 8px;
107
  text-decoration: none;
108
  color: inherit;
109
  text-align: left;
110
  background: var(--nav-landing-card-bg, #e6e6e6);
111
  border: 1px solid var(--border-color, #ddd);
112
  transition: background 0.15s ease, border-color 0.15s ease;
 
113
 
114
  &:hover {
115
  background: var(--nav-landing-card-bg-hover, #dcdcdc);
@@ -120,14 +194,13 @@ html[data-theme='dark'] .nav-landing-card {
120
  border-color: var(--border-color, #333);
121
  }
122
 
123
- // 与卡片同色,不单独铺底
124
  .nav-landing-card-text {
125
  padding: 0;
126
  }
127
 
128
  .nav-landing-card-title {
129
  display: block;
130
- font-size: 0.95rem;
131
  font-weight: 600;
132
  line-height: var(--nav-landing-line-height);
133
  }
@@ -135,7 +208,7 @@ html[data-theme='dark'] .nav-landing-card {
135
  .nav-landing-card-subtitle {
136
  display: block;
137
  text-align: right;
138
- font-size: 0.82rem;
139
  font-weight: 400;
140
  line-height: var(--nav-landing-line-height);
141
  color: var(--text-muted, #666);
@@ -145,16 +218,14 @@ html[data-theme='dark'] .nav-landing-card {
145
  flex-shrink: 0;
146
  width: 100%;
147
  aspect-ratio: 4 / 3;
148
- border-radius: 6px;
149
  box-sizing: border-box;
150
  background: var(--bg-hover, #f0f0f0);
151
  background-size: cover;
152
- // 裁切时保留左上内容(右侧、下侧被裁)
153
  background-position: left top;
154
  background-repeat: no-repeat;
155
  }
156
 
157
- // content/images:日/夜各一套;LLM Causal Flow 另按 html lang 分中/英,经 webpack 产出 URL
158
  .nav-landing-card[data-nav-page='analysis'] .nav-landing-card-shot {
159
  background-image: url('../content/images/highlight.png');
160
  background-color: var(--bg-hover, #f0f0f0);
@@ -170,15 +241,15 @@ html[data-theme='dark'] .nav-landing-card {
170
  background-color: var(--bg-hover, #f0f0f0);
171
  }
172
 
173
- // LLM Causal Flow:日/夜 × 中/英(见 content/images/dag-*.png)
174
- .nav-landing-card[data-nav-page='genAttribute'] .nav-landing-card-shot {
175
- background-image: url('../content/images/dag-en.png');
 
176
  background-color: var(--bg-hover, #f0f0f0);
177
  }
178
 
179
- html[lang='zh-CN'] .nav-landing-card[data-nav-page='genAttribute'] .nav-landing-card-shot {
180
- background-image: url('../content/images/dag-cn.png');
181
- background-color: var(--bg-hover, #f0f0f0);
182
  }
183
 
184
  html[data-theme='dark'] .nav-landing-card[data-nav-page='analysis'] .nav-landing-card-shot {
@@ -195,13 +266,3 @@ html[data-theme='dark'] .nav-landing-card[data-nav-page='attribution'] .nav-land
195
  background-image: url('../content/images/attribute-dark.png');
196
  background-color: var(--bg-hover, #2a2a2a);
197
  }
198
-
199
- html[data-theme='dark'] .nav-landing-card[data-nav-page='genAttribute'] .nav-landing-card-shot {
200
- background-image: url('../content/images/dag-dark-en.png');
201
- background-color: var(--bg-hover, #2a2a2a);
202
- }
203
-
204
- html[data-theme='dark'][lang='zh-CN'] .nav-landing-card[data-nav-page='genAttribute'] .nav-landing-card-shot {
205
- background-image: url('../content/images/dag-dark-cn.png');
206
- background-color: var(--bg-hover, #2a2a2a);
207
- }
 
1
  $breakpoint-mobile: 767px;
2
 
3
+ // ----- 导航卡片尺寸(只改此块)-----
4
+ $nav-grid-gap: 0.75rem;
5
+ $nav-card-gap: 0.65rem;
6
+ $nav-card-pad-block: 0.75rem;
7
+ $nav-card-pad-inline: 0.85rem;
8
+ $nav-card-radius: 8px;
9
+ $nav-card-shot-radius: 6px;
10
+ $nav-title-size: 0.95rem;
11
+ $nav-subtitle-size: 0.82rem;
12
+
13
+ $nav-featured-width-narrow: 100%; // 单列竖排时大卡铺满轨宽
14
+ $nav-featured-width-wide: 60%; // 宽屏首行内宽度,对齐旧版双列中单列占比
15
+
16
+ // 小卡一键标度;只占格宽时用 track,只缩内边距/字时用 inner
17
+ $nav-compact-track-fraction: 0.9;
18
+ $nav-compact-inner-scale: 0.9;
19
+ // -----
20
+
21
  body.nav-landing-page {
22
  --nav-landing-title-size: 22px;
23
  --nav-landing-title-weight: 600;
 
28
  --nav-landing-card-bg: #eeeeee;
29
  --nav-landing-card-bg-hover: #e0e0e0;
30
 
 
 
31
  position: relative;
32
  min-height: 100vh;
33
  height: auto;
 
40
  background: var(--app-bg, #f5f5f5);
41
  }
42
 
 
43
  .nav-landing-settings {
44
  position: absolute;
45
  top: max(12px, env(safe-area-inset-top));
 
57
 
58
  .nav-landing-main {
59
  width: 100%;
 
60
  max-width: 44rem;
61
  box-sizing: border-box;
62
  padding: 2rem 1.25rem;
63
  }
64
 
 
65
  @media (min-width: $breakpoint-mobile + 1) {
66
  .nav-landing-main {
67
  max-width: 64rem;
 
98
  letter-spacing: 0.02em;
99
  }
100
 
101
+ // 首行一张 featured,末行三张 compact;单列时竖排同序
102
  .nav-landing-grid {
103
  display: grid;
104
+ gap: $nav-grid-gap;
105
+ justify-items: center;
106
+ grid-template-columns: 1fr;
107
+ grid-template-areas:
108
+ 'hero'
109
+ 'c1'
110
+ 'c2'
111
+ 'c3';
112
  }
113
 
114
+ // 宽屏:首行大卡跨三列,末行三列各放一张小卡
115
+ @media (min-width: $breakpoint-mobile + 1) {
116
  .nav-landing-grid {
117
+ grid-template-columns: repeat(3, minmax(0, 1fr));
118
+ grid-template-areas:
119
+ 'hero hero hero'
120
+ 'c1 c2 c3';
121
+ }
122
+ }
123
+
124
+ // index.html 顺序:nth-child(1) featured,(2)(3)(4) 为三张 compact
125
+ .nav-landing-card--featured {
126
+ grid-area: hero;
127
+ width: $nav-featured-width-narrow;
128
+ justify-self: stretch;
129
+ }
130
+
131
+ @media (min-width: $breakpoint-mobile + 1) {
132
+ .nav-landing-card--featured {
133
+ width: $nav-featured-width-wide;
134
+ justify-self: center; // 在跨三列的首行里水平居中
135
+ }
136
+ }
137
+
138
+ // 三张 compact 对应第二行 c1–c3(与 grid-template-areas 一致)
139
+ .nav-landing-card.nav-landing-card--compact:nth-child(2) {
140
+ grid-area: c1;
141
+ width: #{$nav-compact-track-fraction * 100%};
142
+ }
143
+
144
+ .nav-landing-card.nav-landing-card--compact:nth-child(3) {
145
+ grid-area: c2;
146
+ width: #{$nav-compact-track-fraction * 100%};
147
+ }
148
+
149
+ .nav-landing-card.nav-landing-card--compact:nth-child(4) {
150
+ grid-area: c3;
151
+ width: #{$nav-compact-track-fraction * 100%};
152
+ }
153
+
154
+ // 双类链:特异性高于单独 .nav-landing-card,避免基类盖掉小卡的 padding/gap
155
+ .nav-landing-card.nav-landing-card--compact {
156
+ padding: #{$nav-card-pad-block * $nav-compact-inner-scale} #{$nav-card-pad-inline * $nav-compact-inner-scale};
157
+ gap: #{$nav-card-gap * $nav-compact-inner-scale};
158
+ border-radius: #{$nav-card-radius * $nav-compact-inner-scale};
159
+
160
+ .nav-landing-card-title {
161
+ font-size: #{$nav-title-size * $nav-compact-inner-scale};
162
+ }
163
+
164
+ .nav-landing-card-subtitle {
165
+ font-size: #{$nav-subtitle-size * $nav-compact-inner-scale};
166
+ }
167
+
168
+ .nav-landing-card-shot {
169
+ border-radius: #{$nav-card-shot-radius * $nav-compact-inner-scale};
170
  }
171
  }
172
 
173
+ // 大卡与小卡共用基类;小卡另加 .nav-landing-card--compact 覆写尺寸
174
  .nav-landing-card {
175
  display: flex;
176
  flex-direction: column;
177
+ gap: $nav-card-gap;
178
+ padding: $nav-card-pad-block $nav-card-pad-inline;
179
+ border-radius: $nav-card-radius;
180
  text-decoration: none;
181
  color: inherit;
182
  text-align: left;
183
  background: var(--nav-landing-card-bg, #e6e6e6);
184
  border: 1px solid var(--border-color, #ddd);
185
  transition: background 0.15s ease, border-color 0.15s ease;
186
+ box-sizing: border-box;
187
 
188
  &:hover {
189
  background: var(--nav-landing-card-bg-hover, #dcdcdc);
 
194
  border-color: var(--border-color, #333);
195
  }
196
 
 
197
  .nav-landing-card-text {
198
  padding: 0;
199
  }
200
 
201
  .nav-landing-card-title {
202
  display: block;
203
+ font-size: $nav-title-size;
204
  font-weight: 600;
205
  line-height: var(--nav-landing-line-height);
206
  }
 
208
  .nav-landing-card-subtitle {
209
  display: block;
210
  text-align: right;
211
+ font-size: $nav-subtitle-size;
212
  font-weight: 400;
213
  line-height: var(--nav-landing-line-height);
214
  color: var(--text-muted, #666);
 
218
  flex-shrink: 0;
219
  width: 100%;
220
  aspect-ratio: 4 / 3;
221
+ border-radius: $nav-card-shot-radius;
222
  box-sizing: border-box;
223
  background: var(--bg-hover, #f0f0f0);
224
  background-size: cover;
 
225
  background-position: left top;
226
  background-repeat: no-repeat;
227
  }
228
 
 
229
  .nav-landing-card[data-nav-page='analysis'] .nav-landing-card-shot {
230
  background-image: url('../content/images/highlight.png');
231
  background-color: var(--bg-hover, #f0f0f0);
 
241
  background-color: var(--bg-hover, #f0f0f0);
242
  }
243
 
244
+ .nav-landing-card[data-nav-page='genAttribute'] video.nav-landing-card-shot {
245
+ object-fit: cover;
246
+ object-position: left top;
247
+ display: block;
248
  background-color: var(--bg-hover, #f0f0f0);
249
  }
250
 
251
+ html[data-theme='dark'] .nav-landing-card[data-nav-page='genAttribute'] video.nav-landing-card-shot {
252
+ background-color: var(--bg-hover, #2a2a2a);
 
253
  }
254
 
255
  html[data-theme='dark'] .nav-landing-card[data-nav-page='analysis'] .nav-landing-card-shot {
 
266
  background-image: url('../content/images/attribute-dark.png');
267
  background-color: var(--bg-hover, #2a2a2a);
268
  }
 
 
 
 
 
 
 
 
 
 
client/src/gen_attribute.html CHANGED
@@ -16,7 +16,7 @@
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
- <a href="index.html" class="home-link" title="InfoLens Home" data-i18n="text,title">InfoLens Home</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
 
16
  <header class="app-page-toolbar app-page-toolbar--bleed">
17
  <h1 class="page-toolbar-title"><span class="title-main-line"><span data-page-title data-i18n></span><span class="title-tagline" data-page-subtitle data-i18n></span></span></h1>
18
  <div class="app-page-toolbar-actions">
19
+ <a href="index.html" class="home-link" title="Info Lens" data-i18n="text,title">Info Lens</a>
20
  <div class="settings-menu-wrapper">
21
  <button id="settings_btn" class="settings-btn" title="Settings" data-i18n="title">
22
  <span class="settings-icon">⚙️</span>
client/src/index.html CHANGED
@@ -28,10 +28,10 @@
28
  <p class="nav-landing-subtitle" data-page-subtitle data-i18n></p>
29
  </header>
30
  <nav class="nav-landing-grid" aria-label="App pages">
31
- <a class="nav-landing-card" href="gen_attribute.html" target="_blank" rel="noopener" data-nav-page="genAttribute" data-i18n="title"></a>
32
- <a class="nav-landing-card" href="analysis.html" target="_blank" rel="noopener" data-nav-page="analysis" data-i18n="title"></a>
33
- <a class="nav-landing-card" href="attribution.html" target="_blank" rel="noopener" data-nav-page="attribution" data-i18n="title"></a>
34
- <a class="nav-landing-card" href="chat.html" target="_blank" rel="noopener" data-nav-page="chat" data-i18n="title"></a>
35
  </nav>
36
  <p class="nav-landing-hint" data-page-formula data-i18n></p>
37
  </main>
 
28
  <p class="nav-landing-subtitle" data-page-subtitle data-i18n></p>
29
  </header>
30
  <nav class="nav-landing-grid" aria-label="App pages">
31
+ <a class="nav-landing-card nav-landing-card--featured" href="gen_attribute.html" target="_blank" rel="noopener" data-nav-page="genAttribute" data-i18n="title"></a>
32
+ <a class="nav-landing-card nav-landing-card--compact" href="analysis.html" target="_blank" rel="noopener" data-nav-page="analysis" data-i18n="title"></a>
33
+ <a class="nav-landing-card nav-landing-card--compact" href="attribution.html" target="_blank" rel="noopener" data-nav-page="attribution" data-i18n="title"></a>
34
+ <a class="nav-landing-card nav-landing-card--compact" href="chat.html" target="_blank" rel="noopener" data-nav-page="chat" data-i18n="title"></a>
35
  </nav>
36
  <p class="nav-landing-hint" data-page-formula data-i18n></p>
37
  </main>
client/src/scripts/genAttributeDemoManifestPlugin.js CHANGED
@@ -1,38 +1,52 @@
1
  /**
2
- * 构建扫描 `demos/gen_attribute/*.json`(源目录)在产物中生成 `demos/gen_attribute/manifest.json`(仅列 slug),供运行时用 fetch 列表。
3
- * 不随 JS bundle 内联大 JSON。
4
  */
5
  const path = require('path');
6
  const fs = require('fs');
7
- const webpack = require('webpack');
8
 
9
  const REL_DIR = 'demos/gen_attribute';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  class GenAttributeDemoManifestPlugin {
12
  apply(compiler) {
13
  const srcDir = path.join(__dirname, '..', REL_DIR);
 
 
 
 
 
 
 
 
 
 
 
14
  compiler.hooks.thisCompilation.tap('GenAttributeDemoManifestPlugin', (compilation) => {
15
- compilation.hooks.processAssets.tap(
16
- {
17
- name: 'GenAttributeDemoManifestPlugin',
18
- stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
19
- },
20
- () => {
21
- let slugs = [];
22
- if (fs.existsSync(srcDir)) {
23
- const files = fs.readdirSync(srcDir).filter((f) => f.endsWith('.json'));
24
- slugs = files
25
- .map((f) => f.replace(/\.json$/i, ''))
26
- .filter((s) => s.length > 0);
27
- slugs.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
28
- }
29
- const json = JSON.stringify({ slugs });
30
- compilation.emitAsset(
31
- path.posix.join(REL_DIR, 'manifest.json'),
32
- new webpack.sources.RawSource(json)
33
- );
34
- }
35
- );
36
  });
37
  }
38
  }
 
1
  /**
2
+ * 构建扫描 `demos/gen_attribute/*.json`,写入 `ts/demos/genAttributeBundledDemoManifest.generated.ts`,供 bundle 内联 slug 列表。
3
+ * 并为该目录注册 contextDependencies,便于 watch 下增减 demo JSON 时触发重编
4
  */
5
  const path = require('path');
6
  const fs = require('fs');
 
7
 
8
  const REL_DIR = 'demos/gen_attribute';
9
+ const GENERATED_BASENAME = 'genAttributeBundledDemoManifest.generated.ts';
10
+
11
+ function collectSlugs(srcDir) {
12
+ if (!fs.existsSync(srcDir)) return [];
13
+ const files = fs.readdirSync(srcDir).filter((f) => f.endsWith('.json'));
14
+ const slugs = files
15
+ .map((f) => f.replace(/\.json$/i, ''))
16
+ .filter((s) => s.length > 0);
17
+ // UTF-16 码元序,与 locale 无关,避免不同构建机生成顺序不一致
18
+ slugs.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
19
+ return slugs;
20
+ }
21
+
22
+ function writeGeneratedModule(srcDir, outPath) {
23
+ const slugs = collectSlugs(srcDir);
24
+ const content =
25
+ '/**\n' +
26
+ ' * Generated by GenAttributeDemoManifestPlugin — do not edit.\n' +
27
+ ' */\n' +
28
+ `export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ${JSON.stringify(slugs)};\n`;
29
+ if (fs.existsSync(outPath) && fs.readFileSync(outPath, 'utf8') === content) return;
30
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
31
+ fs.writeFileSync(outPath, content, 'utf8');
32
+ }
33
 
34
  class GenAttributeDemoManifestPlugin {
35
  apply(compiler) {
36
  const srcDir = path.join(__dirname, '..', REL_DIR);
37
+ const outPath = path.join(__dirname, '..', 'ts', 'demos', GENERATED_BASENAME);
38
+
39
+ compiler.hooks.beforeCompile.tapAsync('GenAttributeDemoManifestPlugin', (_params, callback) => {
40
+ try {
41
+ writeGeneratedModule(srcDir, outPath);
42
+ callback();
43
+ } catch (e) {
44
+ callback(e);
45
+ }
46
+ });
47
+
48
  compiler.hooks.thisCompilation.tap('GenAttributeDemoManifestPlugin', (compilation) => {
49
+ compilation.contextDependencies.add(srcDir);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  });
51
  }
52
  }
client/src/scripts/injectPageMetaIntoHtml.js CHANGED
@@ -103,12 +103,16 @@ function injectPageMeta(html, pageKey, doc) {
103
  } else {
104
  openTag = openTag.replace(/>$/, ` title="${escapeHtmlText(navTitle)}">`);
105
  }
 
 
 
 
106
  const inner =
107
  `<div class="nav-landing-card-text">` +
108
  `<span class="nav-landing-card-title" data-i18n>${escapeHtmlText(navMeta.title)}</span>` +
109
  `<span class="nav-landing-card-subtitle" data-i18n>${escapeHtmlText(navMeta.subtitle)}</span>` +
110
  `</div>` +
111
- `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
112
  html = html.replace(re, `${openTag}${inner}${m[3]}`);
113
  }
114
  }
 
103
  } else {
104
  openTag = openTag.replace(/>$/, ` title="${escapeHtmlText(navTitle)}">`);
105
  }
106
+ const shot =
107
+ navKey === 'genAttribute'
108
+ ? `<video class="nav-landing-card-shot" muted loop playsinline autoplay preload="metadata" aria-hidden="true"></video>`
109
+ : `<div class="nav-landing-card-shot" aria-hidden="true"></div>`;
110
  const inner =
111
  `<div class="nav-landing-card-text">` +
112
  `<span class="nav-landing-card-title" data-i18n>${escapeHtmlText(navMeta.title)}</span>` +
113
  `<span class="nav-landing-card-subtitle" data-i18n>${escapeHtmlText(navMeta.subtitle)}</span>` +
114
  `</div>` +
115
+ shot;
116
  html = html.replace(re, `${openTag}${inner}${m[3]}`);
117
  }
118
  }
client/src/ts/demos/genAttributeBundledDemoManifest.generated.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /**
2
+ * Generated by GenAttributeDemoManifestPlugin — do not edit.
3
+ */
4
+ export const GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS: readonly string[] = ["Write a sonnet about love","写一首绝句,主题是春天","总结","翻译"];
client/src/ts/demos/genAttributeBundledDemos.ts CHANGED
@@ -1,10 +1,10 @@
1
  /**
2
- * Gen Attribute 打包 demo:JSON 在 `dist/demos/gen_attribute/`,运行时 fetch,不打入 bundle
3
- * 列表来自同目录 `manifest.json`(构建时由 webpack 插件生成)。
4
  */
5
 
6
  import type { GenAttrCachedRun } from '../storage/genAttributeRunCache';
7
  import { isKnownPersistedCompletionReason } from '../utils/generationEndReasonLabel';
 
8
 
9
  const BASE = 'demos/gen_attribute/';
10
 
@@ -43,39 +43,11 @@ function isValidGenAttrCachedRunPayload(v: unknown): v is GenAttrCachedRun {
43
  const payloadCache = new Map<string, GenAttrCachedRun>();
44
  const payloadInflight = new Map<string, Promise<GenAttrCachedRun | undefined>>();
45
 
46
- type BundledDemoListEntry = { id: string; label: string };
47
 
48
- let manifestListCache: readonly BundledDemoListEntry[] | undefined;
49
- let manifestListInflight: Promise<readonly BundledDemoListEntry[]> | undefined;
50
-
51
- /**
52
- * manifest 构建期固定;本会话内只网络请求一次,并发首次调用会去重。
53
- */
54
- export async function fetchBundledGenAttributeDemoList(): Promise<readonly BundledDemoListEntry[]> {
55
- if (manifestListCache) return manifestListCache;
56
- if (!manifestListInflight) {
57
- manifestListInflight = fetch(new URL('manifest.json', baseUrl()))
58
- .then((r) => {
59
- if (!r.ok) throw new Error(`manifest: ${r.status}`);
60
- return r.json() as Promise<unknown>;
61
- })
62
- .then((j) => {
63
- const slugs =
64
- j != null &&
65
- typeof j === 'object' &&
66
- 'slugs' in j &&
67
- Array.isArray((j as { slugs: unknown }).slugs)
68
- ? (j as { slugs: unknown[] }).slugs.filter((x): x is string => typeof x === 'string')
69
- : [];
70
- const list = slugs.map((slug) => ({ id: slug, label: slug }));
71
- manifestListCache = list;
72
- return list;
73
- })
74
- .finally(() => {
75
- manifestListInflight = undefined;
76
- });
77
- }
78
- return manifestListInflight;
79
  }
80
 
81
  /**
 
1
  /**
2
+ * Gen Attribute 打包 demo:JSON 在 `dist/demos/gen_attribute/`,运行时 fetch;slug 列表构建期内联自 generated 模块
 
3
  */
4
 
5
  import type { GenAttrCachedRun } from '../storage/genAttributeRunCache';
6
  import { isKnownPersistedCompletionReason } from '../utils/generationEndReasonLabel';
7
+ import { GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS } from './genAttributeBundledDemoManifest.generated';
8
 
9
  const BASE = 'demos/gen_attribute/';
10
 
 
43
  const payloadCache = new Map<string, GenAttrCachedRun>();
44
  const payloadInflight = new Map<string, Promise<GenAttrCachedRun | undefined>>();
45
 
46
+ export type BundledDemoListEntry = { id: string; label: string };
47
 
48
+ /** 构建期固定的 bundled demo 列表(与当前 JS 同版本)。 */
49
+ export function getBundledGenAttributeDemoList(): readonly BundledDemoListEntry[] {
50
+ return GEN_ATTRIBUTE_BUNDLED_DEMO_SLUGS.map((slug) => ({ id: slug, label: slug }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
  /**
client/src/ts/gen_attribute.ts CHANGED
@@ -46,7 +46,7 @@ import {
46
  } from './utils/contentUrl';
47
  import {
48
  fetchBundledGenAttributeDemoBySlug,
49
- fetchBundledGenAttributeDemoList,
50
  isGenAttrRunPayloadValidForUi,
51
  } from './demos/genAttributeBundledDemos';
52
  import { extractErrorMessage } from './utils/errorUtils';
@@ -865,8 +865,8 @@ async function restoreGenAttrFromDemoSlug(slug: string): Promise<void> {
865
  const genAttrCachedHistoryBtn = document.getElementById('gen_attr_cached_history_btn');
866
  let genAttrBundledDemoEntries: Array<{ id: string; label: string }> = [];
867
 
868
- async function refreshGenAttrBundledDemoEntriesList(): Promise<void> {
869
- genAttrBundledDemoEntries = [...(await fetchBundledGenAttributeDemoList())];
870
  }
871
 
872
  const genCachedHistory = initCachedHistoryQueryDropdown({
@@ -898,9 +898,7 @@ initQueryHistoryDropdown({
898
  applyHistoryOnHover: true,
899
  });
900
 
901
- void refreshGenAttrBundledDemoEntriesList().catch((e) => {
902
- console.warn('[gen_attribute] bundled demo manifest prefetch failed', e);
903
- });
904
 
905
  // --- 进度与指标 ---
906
  function showProgress(current: number, total: number): void {
 
46
  } from './utils/contentUrl';
47
  import {
48
  fetchBundledGenAttributeDemoBySlug,
49
+ getBundledGenAttributeDemoList,
50
  isGenAttrRunPayloadValidForUi,
51
  } from './demos/genAttributeBundledDemos';
52
  import { extractErrorMessage } from './utils/errorUtils';
 
865
  const genAttrCachedHistoryBtn = document.getElementById('gen_attr_cached_history_btn');
866
  let genAttrBundledDemoEntries: Array<{ id: string; label: string }> = [];
867
 
868
+ function refreshGenAttrBundledDemoEntriesList(): void {
869
+ genAttrBundledDemoEntries = [...getBundledGenAttributeDemoList()];
870
  }
871
 
872
  const genCachedHistory = initCachedHistoryQueryDropdown({
 
898
  applyHistoryOnHover: true,
899
  });
900
 
901
+ refreshGenAttrBundledDemoEntriesList();
 
 
902
 
903
  // --- 进度与指标 ---
904
  function showProgress(current: number, total: number): void {
client/src/ts/home.ts CHANGED
@@ -2,10 +2,12 @@
2
  * 极简导航首页:主题与语言与 analysis 等页通过 localStorage 一致。
3
  */
4
  import './utils/d3-polyfill';
 
 
5
  import '../css/start.scss';
6
  import '../css/home.scss';
7
 
8
- import { initThemeManager } from './ui/theme';
9
  import { initLanguageManager } from './ui/language';
10
  import { getCurrentLanguage, initI18n, tr } from './lang/i18n-lite';
11
  import { AdminManager } from './utils/adminManager';
@@ -28,6 +30,19 @@ function applyGenAttributeNavCardHref(): void {
28
  a.setAttribute('href', `gen_attribute.html?demo=${encodeURIComponent(slug)}`);
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  applyGenAttributeNavCardHref();
32
 
33
  const apiPrefix = URLHandler.parameters['api'] || '';
@@ -35,7 +50,7 @@ const { api } = initializeCommonApp(apiPrefix);
35
  const adminManager = AdminManager.getInstance();
36
  api.setAdminToken(adminManager.isInAdminMode() ? adminManager.getAdminToken() : null);
37
 
38
- const themeManager = initThemeManager({}, '#theme_dropdown');
39
  const languageManager = initLanguageManager({}, '#language_dropdown');
40
 
41
  void new SettingsMenuManager(
 
2
  * 极简导航首页:主题与语言与 analysis 等页通过 localStorage 一致。
3
  */
4
  import './utils/d3-polyfill';
5
+ import dagPreviewLight from '../content/images/dag.mov';
6
+ import dagPreviewDark from '../content/images/dag-dark.mov';
7
  import '../css/start.scss';
8
  import '../css/home.scss';
9
 
10
+ import { initThemeManager, type Theme } from './ui/theme';
11
  import { initLanguageManager } from './ui/language';
12
  import { getCurrentLanguage, initI18n, tr } from './lang/i18n-lite';
13
  import { AdminManager } from './utils/adminManager';
 
30
  a.setAttribute('href', `gen_attribute.html?demo=${encodeURIComponent(slug)}`);
31
  }
32
 
33
+ const DAG_PREVIEW_BY_THEME: Record<Theme, string> = {
34
+ light: dagPreviewLight,
35
+ dark: dagPreviewDark,
36
+ };
37
+
38
+ function syncGenAttributeCardPreviewVideo(theme: Theme): void {
39
+ const v = document.querySelector<HTMLVideoElement>(
40
+ 'a.nav-landing-card[data-nav-page="genAttribute"] video.nav-landing-card-shot'
41
+ );
42
+ if (!v) return;
43
+ v.src = DAG_PREVIEW_BY_THEME[theme];
44
+ }
45
+
46
  applyGenAttributeNavCardHref();
47
 
48
  const apiPrefix = URLHandler.parameters['api'] || '';
 
50
  const adminManager = AdminManager.getInstance();
51
  api.setAdminToken(adminManager.isInAdminMode() ? adminManager.getAdminToken() : null);
52
 
53
+ const themeManager = initThemeManager({ onThemeChange: syncGenAttributeCardPreviewVideo }, '#theme_dropdown');
54
  const languageManager = initLanguageManager({}, '#language_dropdown');
55
 
56
  void new SettingsMenuManager(
client/src/ts/lang/translations.ts CHANGED
@@ -215,7 +215,6 @@ export const translations: Translations = {
215
  'This action cannot be undone.': '此操作不可撤销。',
216
 
217
  // ========== Demo 对比页面 ==========
218
- 'InfoLens Home': '返回首页',
219
  'Import Result': '导入结果',
220
  'Diff Mode': '差分模式',
221
  'Move left': '左移',
 
215
  'This action cannot be undone.': '此操作不可撤销。',
216
 
217
  // ========== Demo 对比页面 ==========
 
218
  'Import Result': '导入结果',
219
  'Diff Mode': '差分模式',
220
  'Move left': '左移',
client/src/ts/types/html.d.ts CHANGED
@@ -6,3 +6,8 @@ declare module '*.html' {
6
  const content: string;
7
  export default content;
8
  }
 
 
 
 
 
 
6
  const content: string;
7
  export default content;
8
  }
9
+
10
+ declare module '*.mov' {
11
+ const url: string;
12
+ export default url;
13
+ }
client/src/webpack.config.js CHANGED
@@ -73,6 +73,10 @@ module.exports = {
73
  }
74
  }
75
  },
 
 
 
 
76
  {
77
  test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
78
  type: 'asset',
 
73
  }
74
  }
75
  },
76
+ {
77
+ test: /\.mov$/,
78
+ type: 'asset/resource'
79
+ },
80
  {
81
  test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
82
  type: 'asset',