qqyule commited on
Commit
783b7b3
·
1 Parent(s): bc02199

feat: polish object archive ui

Browse files
docs/03-dev-schedule.md CHANGED
@@ -137,12 +137,14 @@ Right: Secret Diary
137
  Bottom: Share Card + Trace
138
  ```
139
 
140
- - [ ] 自定义 CSS
141
- - [ ] 自定义 hero section
142
- - [ ] 隐藏 Gradio 默认风格
143
- - [ ] 加 typewriter animation
144
- - [ ] 做英文主文案 + 中文辅助
145
- - [ ] 做 6 个示例卡片
 
 
146
 
147
  ---
148
 
 
137
  Bottom: Share Card + Trace
138
  ```
139
 
140
+ - [x] 自定义 CSS
141
+ - [x] 自定义 hero section
142
+ - [x] 隐藏 Gradio 默认风格
143
+ - [x] 加 typewriter / archive reveal 视觉感
144
+ - [x] 做英文主文案 + 中文辅助
145
+ - [x] 做 6 个示例卡片
146
+
147
+ 完成记录:Phase 2 UI 已完成为 mock runtime archive dashboard。仍未接入真实 VLM、llama.cpp、LoRA 或 Hugging Face Space;`UI 参考/` 仅作为本地视觉参考,不入库。
148
 
149
  ---
150
 
docs/INITIAL_STAGE_REPORT.md CHANGED
@@ -86,7 +86,7 @@ OK
86
  ## Current Limitations
87
 
88
  - The app still uses mock model outputs.
89
- - UI polish remains intentionally unfinished.
90
  - Sample traces are mock traces, not real model traces.
91
  - Remote repo and hosted Space are not created yet.
92
 
 
86
  ## Current Limitations
87
 
88
  - The app still uses mock model outputs.
89
+ - Phase 2 UI polish is complete, but it still runs on the mock runtime.
90
  - Sample traces are mock traces, not real model traces.
91
  - Remote repo and hosted Space are not created yet.
92
 
docs/PHASE2_UI_REPORT.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 2 UI Report
2
+
3
+ ## Scope
4
+
5
+ Phase 2 polishes the mock MVP into an archive-style Gradio interface while preserving the current deterministic pipeline.
6
+
7
+ Completed:
8
+
9
+ - archive dashboard layout: Object Intake, Object File, Secret Diary, Share Card, Object Chat, and Trace
10
+ - English-first / Chinese-second UI hierarchy
11
+ - dark paper archive styling with amber highlights and museum-label panels
12
+ - segmented personality mode control
13
+ - six visible example object cards that trigger stable mock generation
14
+ - empty states for Object File, Diary, Share Card, and Trace
15
+ - UI error fallback that exposes the exception type and message without hiding generation context
16
+ - screenshot-friendly share card rendering
17
+ - mobile single-column layout for narrow screens
18
+
19
+ Not included:
20
+
21
+ - real MiniCPM-V or other VLM integration
22
+ - real llama.cpp / llama-cpp-python text runtime
23
+ - LoRA fine-tuning
24
+ - Hugging Face Space deployment
25
+ - GitHub remote creation
26
+ - external image assets or production dependencies
27
+
28
+ ## Reference Boundary
29
+
30
+ The local `UI 参考/` images were used only for visual direction: archive panels, dark paper texture, amber linework, example cards, and museum-label hierarchy.
31
+
32
+ `UI 参考/` remains ignored by Git and must not be committed.
33
+
34
+ ## Runtime Boundary
35
+
36
+ The app still runs on the mock runtime. The UI calls the existing `src/pipeline.py` boundary and does not change model behavior.
37
+
38
+ ## Verification Targets
39
+
40
+ - desktop preview at 1440x1000
41
+ - mobile preview at 390x844
42
+ - six example object cards generate object JSON, persona JSON, diary, share card, and trace
43
+ - manual description plus personality mode still generates successfully
44
+ - chat shows "Wake an object first. / 请先唤醒一个物品。" before generation
docs/README.md CHANGED
@@ -21,5 +21,6 @@ This folder contains the planning source of truth for Objectverse Diary.
21
  - `DATASET.md`: SFT preview schema, generation workflow, curation checklist, and publishing notes.
22
  - `FAILURES.md`: failure record template and anticipated non-UI fallback cases.
23
  - `INITIAL_STAGE_REPORT.md`: local initial-stage completion evidence and acceptance commands.
 
24
  - `EXTERNAL_SETUP.md`: GitHub and Hugging Face Space setup notes requiring confirmation.
25
  - `SUBMISSION_GUIDE.md`: final submission checklist.
 
21
  - `DATASET.md`: SFT preview schema, generation workflow, curation checklist, and publishing notes.
22
  - `FAILURES.md`: failure record template and anticipated non-UI fallback cases.
23
  - `INITIAL_STAGE_REPORT.md`: local initial-stage completion evidence and acceptance commands.
24
+ - `PHASE2_UI_REPORT.md`: archive UI completion scope, runtime boundary, and verification targets.
25
  - `EXTERNAL_SETUP.md`: GitHub and Hugging Face Space setup notes requiring confirmation.
26
  - `SUBMISSION_GUIDE.md`: final submission checklist.
src/examples.py CHANGED
@@ -2,37 +2,58 @@
2
 
3
  EXAMPLE_OBJECTS = [
4
  {
 
5
  "label": "Coffee mug / 咖啡杯",
6
  "description": "old white coffee mug on a developer desk",
7
  "mode": "Cynical",
 
8
  },
9
  {
 
10
  "label": "Mechanical keyboard / 机械键盘",
11
  "description": "dusty black mechanical keyboard in an office",
12
  "mode": "Philosopher",
 
13
  },
14
  {
 
15
  "label": "Running shoe / 跑鞋",
16
  "description": "worn running shoe near the bedroom door",
17
  "mode": "Lonely",
 
18
  },
19
  {
 
20
  "label": "Desk lamp / 台灯",
21
  "description": "metal desk lamp over late night notes",
22
  "mode": "Dramatic",
 
23
  },
24
  {
 
25
  "label": "Water bottle / 水瓶",
26
  "description": "clear plastic water bottle on a kitchen counter",
27
  "mode": "Romantic",
 
28
  },
29
  {
 
30
  "label": "Notebook / 笔记本",
31
  "description": "old notebook of abandoned project ideas",
32
  "mode": "Cynical",
 
33
  },
34
  ]
35
 
36
 
37
  def gradio_examples() -> list[list[str]]:
38
  return [[item["description"]] for item in EXAMPLE_OBJECTS]
 
 
 
 
 
 
 
 
 
 
2
 
3
  EXAMPLE_OBJECTS = [
4
  {
5
+ "archive_id": "OVD-001",
6
  "label": "Coffee mug / 咖啡杯",
7
  "description": "old white coffee mug on a developer desk",
8
  "mode": "Cynical",
9
+ "tags": "Everyday · Warmth",
10
  },
11
  {
12
+ "archive_id": "OVD-002",
13
  "label": "Mechanical keyboard / 机械键盘",
14
  "description": "dusty black mechanical keyboard in an office",
15
  "mode": "Philosopher",
16
+ "tags": "Work · Habit",
17
  },
18
  {
19
+ "archive_id": "OVD-003",
20
  "label": "Running shoe / 跑鞋",
21
  "description": "worn running shoe near the bedroom door",
22
  "mode": "Lonely",
23
+ "tags": "Journey · Wear",
24
  },
25
  {
26
+ "archive_id": "OVD-004",
27
  "label": "Desk lamp / 台灯",
28
  "description": "metal desk lamp over late night notes",
29
  "mode": "Dramatic",
30
+ "tags": "Night · Focus",
31
  },
32
  {
33
+ "archive_id": "OVD-005",
34
  "label": "Water bottle / 水瓶",
35
  "description": "clear plastic water bottle on a kitchen counter",
36
  "mode": "Romantic",
37
+ "tags": "Routine · Clear",
38
  },
39
  {
40
+ "archive_id": "OVD-006",
41
  "label": "Notebook / 笔记本",
42
  "description": "old notebook of abandoned project ideas",
43
  "mode": "Cynical",
44
+ "tags": "Memory · Drafts",
45
  },
46
  ]
47
 
48
 
49
  def gradio_examples() -> list[list[str]]:
50
  return [[item["description"]] for item in EXAMPLE_OBJECTS]
51
+
52
+
53
+ def example_button_label(index: int) -> str:
54
+ item = EXAMPLE_OBJECTS[index]
55
+ return (
56
+ f"{item['archive_id']}\n"
57
+ f"{item['label']}\n"
58
+ f"{item['mode']} · {item['tags']}"
59
+ )
src/renderer/share_card.py CHANGED
@@ -13,10 +13,16 @@ def render_share_card(persona: PersonaEnvelope, diary: DiaryEntry) -> str:
13
  tags = "".join(f"<span>{escape(tag)}</span>" for tag in p.tags)
14
  return f"""
15
  <article class="{CARD_WRAPPER_CLASS}">
16
- <div class="card-kicker">Objectverse Diary / 万物日记</div>
17
- <h2>{escape(p.character_name)}</h2>
 
 
 
 
 
18
  <p class="card-object">{escape(p.object_name)} · {escape(p.mood)}</p>
19
  <p class="card-quote">{escape(diary.english)}</p>
 
20
  <div class="card-tags">{tags}</div>
21
  </article>
22
  """
 
13
  tags = "".join(f"<span>{escape(tag)}</span>" for tag in p.tags)
14
  return f"""
15
  <article class="{CARD_WRAPPER_CLASS}">
16
+ <header class="card-header">
17
+ <div>
18
+ <div class="card-kicker">Objectverse Diary / 万物日记</div>
19
+ <h2>{escape(p.character_name)}</h2>
20
+ </div>
21
+ <span class="card-stamp">OBJECT FILE</span>
22
+ </header>
23
  <p class="card-object">{escape(p.object_name)} · {escape(p.mood)}</p>
24
  <p class="card-quote">{escape(diary.english)}</p>
25
+ <p class="card-cn">{escape(diary.chinese)}</p>
26
  <div class="card-tags">{tags}</div>
27
  </article>
28
  """
src/ui/layout.py CHANGED
@@ -1,39 +1,100 @@
1
- """Gradio layout for the initial mock MVP."""
2
 
3
  from __future__ import annotations
4
 
 
5
  from pathlib import Path
6
  from typing import Any
7
 
8
  import gradio as gr
9
 
10
  from src.config import APP_TITLE, DEFAULT_MODE, PERSONALITY_MODES
11
- from src.examples import gradio_examples
12
  from src.models.llama_cpp_runner import reply_as_object
 
13
  from src.pipeline import format_diary_markdown, generate_object_diary
14
  from src.renderer.share_card import render_share_card
15
  from src.ui import copy
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def build_app() -> gr.Blocks:
19
  css = Path("src/ui/styles.css").read_text(encoding="utf-8")
20
 
21
- with gr.Blocks(css=css, title=APP_TITLE) as demo:
22
  gr.HTML(
23
  f"""
24
  <section id="objectverse-hero">
25
- <h1>{APP_TITLE}</h1>
26
- <p>Every object has a secret life.<br><span>每个物品都有秘密人生。</span></p>
 
 
 
 
 
 
 
 
 
 
 
 
27
  </section>
28
- """
 
29
  )
30
 
31
  result_state = gr.State()
32
 
33
- with gr.Row():
34
- with gr.Column(scale=4, elem_classes=["archive-panel"]):
 
35
  image_input = gr.Image(
36
  label=copy.UPLOAD_LABEL,
 
37
  type="filepath",
38
  sources=["upload"],
39
  elem_id="object-upload",
@@ -42,63 +103,102 @@ def build_app() -> gr.Blocks:
42
  label=copy.DESCRIPTION_LABEL,
43
  placeholder=copy.DESCRIPTION_PLACEHOLDER,
44
  lines=3,
 
45
  elem_id="object-description",
46
  )
47
- mode_input = gr.Dropdown(
48
  label=copy.MODE_LABEL,
49
  choices=PERSONALITY_MODES,
50
  value=DEFAULT_MODE,
51
  elem_id="personality-mode",
 
52
  )
53
- generate_button = gr.Button(copy.GENERATE_LABEL, variant="primary")
54
- gr.Examples(
55
- examples=gradio_examples(),
56
- inputs=[description_input],
57
- label=copy.EXAMPLES_LABEL,
58
- examples_per_page=6,
59
- elem_id="object-examples",
 
 
 
60
  )
 
 
 
 
 
 
 
 
 
61
 
62
- with gr.Column(scale=6, elem_classes=["archive-panel"]):
63
- object_json = gr.JSON(label=copy.OBJECT_JSON_LABEL)
64
- persona_json = gr.JSON(label=copy.PERSONA_JSON_LABEL)
 
 
 
 
65
 
66
- with gr.Row():
67
- with gr.Column(scale=6, elem_classes=["archive-panel"]):
68
  diary_output = gr.Markdown(
69
- value="### Secret Diary / 秘密日记\n\nWake an object to open its file. / 唤醒物品后打开档案。",
70
  label=copy.DIARY_LABEL,
71
  elem_id="diary-output",
72
  )
73
- with gr.Column(scale=6, elem_classes=["archive-panel"]):
74
- share_card = gr.HTML(
75
- value='<div class="objectverse-placeholder">Share card will appear here. / 分享卡片会显示在这里。</div>',
76
- label=copy.SHARE_CARD_LABEL,
77
- )
78
 
79
- with gr.Row():
80
- with gr.Column(scale=6, elem_classes=["archive-panel"]):
81
- chatbot = gr.Chatbot(label=copy.CHAT_LABEL, type="messages")
 
 
 
 
 
 
 
 
 
 
 
82
  chat_input = gr.Textbox(placeholder=copy.CHAT_INPUT_PLACEHOLDER, show_label=False)
83
- chat_button = gr.Button(copy.CHAT_BUTTON_LABEL)
84
- with gr.Column(scale=6, elem_classes=["archive-panel"]):
85
- trace_json = gr.JSON(label=copy.TRACE_JSON_LABEL)
 
 
 
86
  trace_path = gr.Textbox(label=copy.TRACE_PATH_LABEL, interactive=False)
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  generate_button.click(
89
  fn=generate_object_file,
90
  inputs=[image_input, description_input, mode_input],
91
- outputs=[
92
- object_json,
93
- persona_json,
94
- diary_output,
95
- share_card,
96
- trace_json,
97
- trace_path,
98
- result_state,
99
- chatbot,
100
- ],
101
  )
 
 
 
 
 
 
 
 
102
  chat_button.click(
103
  fn=chat_with_object,
104
  inputs=[chat_input, chatbot, result_state],
@@ -113,36 +213,161 @@ def build_app() -> gr.Blocks:
113
  return demo
114
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  def generate_object_file(
117
  image_path: str | None,
118
  description: str,
119
  mode: str,
120
- ) -> tuple[dict[str, Any], dict[str, Any], str, str, dict[str, Any], str, dict[str, Any], list[dict[str, str]]]:
121
- result = generate_object_diary(image_path, description, mode)
 
 
 
 
 
 
122
  return (
123
- result.object_understanding.model_dump(mode="json"),
124
- result.persona.model_dump(mode="json"),
 
125
  format_diary_markdown(result.diary.title, result.diary.english, result.diary.chinese),
126
  render_share_card(result.persona, result.diary),
 
127
  result.trace.model_dump(mode="json"),
128
  result.trace_path,
129
  result.model_dump(mode="json"),
130
- [],
131
  )
132
 
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  def chat_with_object(
135
  message: str,
136
  history: list[dict[str, str]] | None,
137
  result_state: dict[str, Any] | None,
138
  ) -> tuple[list[dict[str, str]], str]:
139
- history = history or []
140
  clean_message = message.strip()
141
  if not clean_message:
142
  return history, ""
143
 
144
  if not result_state:
145
- reply = "Wake an object first. / 请先唤醒一个物品。"
146
  else:
147
  reply = reply_as_object(result_state["persona"], clean_message)
148
 
 
1
+ """Gradio layout for the mock Objectverse archive UI."""
2
 
3
  from __future__ import annotations
4
 
5
+ from html import escape
6
  from pathlib import Path
7
  from typing import Any
8
 
9
  import gradio as gr
10
 
11
  from src.config import APP_TITLE, DEFAULT_MODE, PERSONALITY_MODES
12
+ from src.examples import EXAMPLE_OBJECTS, example_button_label
13
  from src.models.llama_cpp_runner import reply_as_object
14
+ from src.models.schema import GenerationResult
15
  from src.pipeline import format_diary_markdown, generate_object_diary
16
  from src.renderer.share_card import render_share_card
17
  from src.ui import copy
18
 
19
+ CHAT_EMPTY_MESSAGE = "Wake an object first. / 请先唤醒一个物品。"
20
+
21
+ OBJECT_FILE_EMPTY = """
22
+ <div class="archive-empty">
23
+ <span class="archive-label">Object File / 物品档案</span>
24
+ <h3>No object awake yet.</h3>
25
+ <p>Upload or describe an everyday object to open its secret archive. / 上传或描述一个日常物品后打开秘密档案。</p>
26
+ </div>
27
+ """
28
+
29
+ DIARY_EMPTY = """
30
+ ### Secret Diary / 秘密日记
31
+
32
+ Wake an object to open its diary. / 唤醒物品后阅读它的日记。
33
+ """
34
+
35
+ SHARE_CARD_EMPTY = """
36
+ <div class="objectverse-placeholder">
37
+ <span>Share Card / 分享卡片</span>
38
+ <strong>Waiting for an object file.</strong>
39
+ <p>A screenshot-friendly archive card will appear here. / 可截图分享的档案卡片会显示在这里。</p>
40
+ </div>
41
+ """
42
+
43
+ TRACE_EMPTY = """
44
+ <div class="archive-empty compact">
45
+ <span class="archive-label">Trace / 模型轨迹</span>
46
+ <p>No trace saved yet. / 尚未保存 trace。</p>
47
+ </div>
48
+ """
49
+
50
+ GenerationUiResult = tuple[
51
+ str,
52
+ dict[str, Any],
53
+ dict[str, Any],
54
+ str,
55
+ str,
56
+ str,
57
+ dict[str, Any],
58
+ str,
59
+ dict[str, Any] | None,
60
+ list[dict[str, str]],
61
+ ]
62
+
63
 
64
  def build_app() -> gr.Blocks:
65
  css = Path("src/ui/styles.css").read_text(encoding="utf-8")
66
 
67
+ with gr.Blocks(head=f"<style>{css}</style>", title=APP_TITLE, fill_width=True) as demo:
68
  gr.HTML(
69
  f"""
70
  <section id="objectverse-hero">
71
+ <div class="hero-mark">
72
+ <span>OVD</span>
73
+ <small>000827</small>
74
+ </div>
75
+ <div class="hero-copy">
76
+ <p class="hero-kicker">Local small-model object archive<br><span>本地小模型物品档案</span></p>
77
+ <h1>{APP_TITLE}</h1>
78
+ <p>Every object has a secret life.<br><span>每个物品都有秘密人生。</span></p>
79
+ </div>
80
+ <div class="hero-badges" aria-label="Project constraints">
81
+ <span>Small Models</span>
82
+ <span>Local-First</span>
83
+ <span>No Cloud APIs</span>
84
+ </div>
85
  </section>
86
+ """,
87
+ padding=False,
88
  )
89
 
90
  result_state = gr.State()
91
 
92
+ with gr.Row(elem_id="archive-main-grid", elem_classes=["archive-grid"]):
93
+ with gr.Column(scale=4, elem_classes=["archive-panel", "intake-panel"]):
94
+ gr.HTML(_panel_header("01", "Object Intake", "物品接收", "Upload, describe, or pick a sample."), padding=False)
95
  image_input = gr.Image(
96
  label=copy.UPLOAD_LABEL,
97
+ show_label=False,
98
  type="filepath",
99
  sources=["upload"],
100
  elem_id="object-upload",
 
103
  label=copy.DESCRIPTION_LABEL,
104
  placeholder=copy.DESCRIPTION_PLACEHOLDER,
105
  lines=3,
106
+ max_lines=5,
107
  elem_id="object-description",
108
  )
109
+ mode_input = gr.Radio(
110
  label=copy.MODE_LABEL,
111
  choices=PERSONALITY_MODES,
112
  value=DEFAULT_MODE,
113
  elem_id="personality-mode",
114
+ elem_classes=["mode-switch"],
115
  )
116
+ generate_button = gr.Button(copy.GENERATE_LABEL, variant="primary", elem_id="wake-button")
117
+
118
+ gr.HTML(
119
+ """
120
+ <div class="example-section-title">
121
+ <span>Example Objects / 示例物品</span>
122
+ <small>Click a file to generate instantly.</small>
123
+ </div>
124
+ """,
125
+ padding=False,
126
  )
127
+ example_buttons: list[gr.Button] = []
128
+ for index in range(len(EXAMPLE_OBJECTS)):
129
+ example_buttons.append(
130
+ gr.Button(
131
+ example_button_label(index),
132
+ elem_classes=["example-card"],
133
+ variant="secondary",
134
+ )
135
+ )
136
 
137
+ with gr.Column(scale=4, elem_classes=["archive-panel", "file-panel"]):
138
+ gr.HTML(_panel_header("02", "Object File", "物品档案", "Structured mock understanding and persona."), padding=False)
139
+ object_file_summary = gr.HTML(value=OBJECT_FILE_EMPTY, elem_id="object-file-summary", padding=False)
140
+ with gr.Accordion("Raw object understanding JSON / 原始物品识别 JSON", open=False):
141
+ object_json = gr.JSON(value={}, label=copy.OBJECT_JSON_LABEL)
142
+ with gr.Accordion("Raw persona JSON / 原始人格 JSON", open=False):
143
+ persona_json = gr.JSON(value={}, label=copy.PERSONA_JSON_LABEL)
144
 
145
+ with gr.Column(scale=4, elem_classes=["archive-panel", "diary-panel"]):
146
+ gr.HTML(_panel_header("03", "Secret Diary", "秘密日记", "A private note written by the object."), padding=False)
147
  diary_output = gr.Markdown(
148
+ value=DIARY_EMPTY,
149
  label=copy.DIARY_LABEL,
150
  elem_id="diary-output",
151
  )
 
 
 
 
 
152
 
153
+ with gr.Row(elem_id="archive-bottom-grid", elem_classes=["archive-grid", "bottom-grid"]):
154
+ with gr.Column(scale=5, elem_classes=["archive-panel", "share-panel"]):
155
+ gr.HTML(_panel_header("04", "Share Card", "分享卡片", "Fixed-width card for screenshots."), padding=False)
156
+ share_card = gr.HTML(value=SHARE_CARD_EMPTY, label=copy.SHARE_CARD_LABEL, padding=False)
157
+
158
+ with gr.Column(scale=4, elem_classes=["archive-panel", "chat-panel"]):
159
+ gr.HTML(_panel_header("05", "Object Chat", "物品对话", "Ask after the object wakes up."), padding=False)
160
+ chatbot = gr.Chatbot(
161
+ value=_empty_chat_history(),
162
+ label=copy.CHAT_LABEL,
163
+ type="messages",
164
+ height=300,
165
+ allow_tags=False,
166
+ )
167
  chat_input = gr.Textbox(placeholder=copy.CHAT_INPUT_PLACEHOLDER, show_label=False)
168
+ chat_button = gr.Button(copy.CHAT_BUTTON_LABEL, elem_classes=["quiet-button"])
169
+
170
+ with gr.Column(scale=3, elem_classes=["archive-panel", "trace-panel"]):
171
+ gr.HTML(_panel_header("06", "Trace", "模型轨迹", "Saved JSON record for reproducibility."), padding=False)
172
+ trace_summary = gr.HTML(value=TRACE_EMPTY, elem_id="trace-summary", padding=False)
173
+ trace_json = gr.JSON(value={}, label=copy.TRACE_JSON_LABEL)
174
  trace_path = gr.Textbox(label=copy.TRACE_PATH_LABEL, interactive=False)
175
 
176
+ manual_outputs = [
177
+ object_file_summary,
178
+ object_json,
179
+ persona_json,
180
+ diary_output,
181
+ share_card,
182
+ trace_summary,
183
+ trace_json,
184
+ trace_path,
185
+ result_state,
186
+ chatbot,
187
+ ]
188
+
189
  generate_button.click(
190
  fn=generate_object_file,
191
  inputs=[image_input, description_input, mode_input],
192
+ outputs=manual_outputs,
 
 
 
 
 
 
 
 
 
193
  )
194
+
195
+ for index, button in enumerate(example_buttons):
196
+ button.click(
197
+ fn=_example_handler(index),
198
+ inputs=[],
199
+ outputs=[description_input, mode_input, *manual_outputs],
200
+ )
201
+
202
  chat_button.click(
203
  fn=chat_with_object,
204
  inputs=[chat_input, chatbot, result_state],
 
213
  return demo
214
 
215
 
216
+ def _panel_header(index: str, title: str, chinese: str, note: str) -> str:
217
+ return f"""
218
+ <header class="panel-header">
219
+ <span>{escape(index)}</span>
220
+ <div>
221
+ <h2>{escape(title)} <small>{escape(chinese)}</small></h2>
222
+ <p>{escape(note)}</p>
223
+ </div>
224
+ </header>
225
+ """
226
+
227
+
228
+ def _example_handler(index: int):
229
+ def load_example() -> tuple[Any, ...]:
230
+ item = EXAMPLE_OBJECTS[index]
231
+ result = generate_object_file(None, item["description"], item["mode"])
232
+ return item["description"], item["mode"], *result
233
+
234
+ return load_example
235
+
236
+
237
  def generate_object_file(
238
  image_path: str | None,
239
  description: str,
240
  mode: str,
241
+ ) -> GenerationUiResult:
242
+ try:
243
+ result = generate_object_diary(image_path, description, mode)
244
+ except Exception as exc: # pragma: no cover - exercised manually by UI failure paths.
245
+ return _generation_error(exc, description, mode)
246
+
247
+ object_payload = result.object_understanding.model_dump(mode="json")
248
+ persona_payload = result.persona.model_dump(mode="json")
249
  return (
250
+ _render_object_file(result),
251
+ object_payload,
252
+ persona_payload,
253
  format_diary_markdown(result.diary.title, result.diary.english, result.diary.chinese),
254
  render_share_card(result.persona, result.diary),
255
+ _render_trace_summary(result),
256
  result.trace.model_dump(mode="json"),
257
  result.trace_path,
258
  result.model_dump(mode="json"),
259
+ _awake_chat_history(result),
260
  )
261
 
262
 
263
+ def _render_object_file(result: GenerationResult) -> str:
264
+ obj = result.object_understanding.object
265
+ persona = result.persona.persona
266
+ features = "".join(f"<li>{escape(feature)}</li>" for feature in obj.visible_features)
267
+ tags = "".join(f"<span>{escape(tag)}</span>" for tag in persona.tags)
268
+ confidence = f"{obj.confidence:.0%}"
269
+ return f"""
270
+ <article class="object-file-card">
271
+ <div class="file-meta">
272
+ <span>Confidence {escape(confidence)}</span>
273
+ <span>{escape(result.trace.mode)}</span>
274
+ </div>
275
+ <h3>{escape(persona.character_name)}</h3>
276
+ <p class="object-name">{escape(obj.name)} / {escape(persona.object_name)}</p>
277
+ <dl>
278
+ <div>
279
+ <dt>Mood</dt>
280
+ <dd>{escape(persona.mood)}</dd>
281
+ </div>
282
+ <div>
283
+ <dt>Secret fear</dt>
284
+ <dd>{escape(persona.secret_fear)}</dd>
285
+ </div>
286
+ <div>
287
+ <dt>Core memory</dt>
288
+ <dd>{escape(persona.core_memory)}</dd>
289
+ </div>
290
+ </dl>
291
+ <div class="feature-list">
292
+ <strong>Visible features / 可见特征</strong>
293
+ <ul>{features}</ul>
294
+ </div>
295
+ <p class="complaint">{escape(persona.complaint)}</p>
296
+ <div class="file-tags">{tags}</div>
297
+ </article>
298
+ """
299
+
300
+
301
+ def _render_trace_summary(result: GenerationResult) -> str:
302
+ return f"""
303
+ <div class="trace-card">
304
+ <span class="archive-label">Trace saved / Trace 已保存</span>
305
+ <strong>{escape(result.trace.trace_id)}</strong>
306
+ <p>{escape(result.trace.model_runtime["vision"])} · {escape(result.trace.model_runtime["text"])}</p>
307
+ </div>
308
+ """
309
+
310
+
311
+ def _generation_error(exc: Exception, description: str, mode: str) -> GenerationUiResult:
312
+ error_type = type(exc).__name__
313
+ error_message = str(exc) or "Unknown generation error"
314
+ error_payload = {
315
+ "error": error_type,
316
+ "message": error_message,
317
+ "input": {"description": description, "mode": mode},
318
+ }
319
+ error_html = f"""
320
+ <div class="archive-error">
321
+ <span>Generation failed / 生成失败</span>
322
+ <strong>{escape(error_type)}</strong>
323
+ <p>{escape(error_message)}</p>
324
+ </div>
325
+ """
326
+ error_markdown = (
327
+ "### Generation failed / 生成失败\n\n"
328
+ f"{error_type}: {error_message}\n\n"
329
+ "Please try another description or sample object. / 请尝试其他描述或示例物品。"
330
+ )
331
+ return (
332
+ error_html,
333
+ error_payload,
334
+ error_payload,
335
+ error_markdown,
336
+ error_html,
337
+ error_html,
338
+ error_payload,
339
+ "",
340
+ None,
341
+ [{"role": "assistant", "content": f"Generation failed. / 生成失败:{error_type}"}],
342
+ )
343
+
344
+
345
+ def _empty_chat_history() -> list[dict[str, str]]:
346
+ return [{"role": "assistant", "content": CHAT_EMPTY_MESSAGE}]
347
+
348
+
349
+ def _awake_chat_history(result: GenerationResult) -> list[dict[str, str]]:
350
+ name = result.persona.persona.character_name
351
+ return [
352
+ {
353
+ "role": "assistant",
354
+ "content": f"{name} is awake. Ask what it remembers. / {name} 已被唤醒,可以追问它记得什么。",
355
+ }
356
+ ]
357
+
358
+
359
  def chat_with_object(
360
  message: str,
361
  history: list[dict[str, str]] | None,
362
  result_state: dict[str, Any] | None,
363
  ) -> tuple[list[dict[str, str]], str]:
364
+ history = history or _empty_chat_history()
365
  clean_message = message.strip()
366
  if not clean_message:
367
  return history, ""
368
 
369
  if not result_state:
370
+ reply = CHAT_EMPTY_MESSAGE
371
  else:
372
  reply = reply_as_object(result_state["persona"], clean_message)
373
 
src/ui/styles.css CHANGED
@@ -1,169 +1,733 @@
1
- body {
2
- background: #16120e;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
  .gradio-container {
7
  background:
8
- radial-gradient(circle at top left, rgba(191, 138, 64, 0.18), transparent 28rem),
9
- linear-gradient(135deg, #1a1510 0%, #2a2119 48%, #151515 100%);
10
- color: #f3eadc;
 
 
11
  font-family: Georgia, "Times New Roman", serif;
12
- max-width: 100%;
 
 
13
  overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
 
16
  #objectverse-hero {
17
- border: 1px solid rgba(218, 178, 112, 0.28);
18
- background: rgba(28, 22, 16, 0.82);
19
- padding: 22px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
  #objectverse-hero h1 {
23
- color: #f6d38b;
24
- font-size: 34px;
25
- line-height: 1.12;
26
  margin: 0 0 8px;
27
  }
28
 
29
  #objectverse-hero p {
30
- color: #d8c7ad;
31
  font-size: 16px;
 
32
  margin: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
35
- #objectverse-hero span {
36
- color: #d8c7ad;
 
 
37
  }
38
 
39
  .archive-panel {
40
- border: 1px solid rgba(218, 178, 112, 0.24);
41
- background: rgba(24, 20, 16, 0.76);
42
- padding: 14px;
 
 
 
 
43
  }
44
 
45
- .objectverse-card {
46
- border: 1px solid rgba(246, 211, 139, 0.42);
47
- background: #211912;
48
- color: #f4ead8;
49
- padding: 22px;
50
- max-width: 720px;
 
 
51
  }
52
 
53
- .objectverse-card h2 {
54
- color: #f6d38b;
55
- font-size: 28px;
56
- margin: 8px 0;
 
 
 
57
  }
58
 
59
- .card-kicker,
60
- .card-object {
61
- color: #cdb894;
62
- letter-spacing: 0;
 
 
 
 
 
 
63
  }
64
 
65
- .card-quote {
66
- border-left: 3px solid #bf8a40;
67
- color: #f3eadc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  line-height: 1.55;
69
- margin: 18px 0;
70
- padding-left: 14px;
71
  }
72
 
73
- .card-tags {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  display: flex;
75
  flex-wrap: wrap;
76
  gap: 8px;
 
77
  }
78
 
 
 
79
  .card-tags span {
80
- border: 1px solid rgba(246, 211, 139, 0.32);
81
- color: #f6d38b;
82
- padding: 5px 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
- .objectverse-placeholder {
86
- border: 1px dashed rgba(246, 211, 139, 0.28);
87
- color: #cdb894;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  line-height: 1.55;
89
- padding: 18px;
 
 
 
 
 
 
 
 
90
  }
91
 
92
  #diary-output,
93
  #diary-output * {
94
- color: #d8c7ad !important;
 
 
 
 
 
 
 
 
95
  }
96
 
97
  #diary-output h1,
98
  #diary-output h2,
99
  #diary-output h3 {
100
- color: #f6d38b !important;
 
 
 
 
 
101
  }
102
 
103
- #object-examples table {
104
- border-collapse: collapse;
105
- table-layout: fixed;
 
 
 
 
 
 
 
106
  width: 100%;
107
  }
108
 
109
- #object-examples th {
110
- background: #211912 !important;
111
- color: #f6d38b !important;
112
- overflow-wrap: anywhere;
113
- white-space: normal !important;
114
  }
115
 
116
- #object-examples td {
117
- background: #fffaf0 !important;
118
- color: #211912 !important;
119
- line-height: 1.35;
120
- overflow-wrap: anywhere;
121
- white-space: normal !important;
122
  }
123
 
124
- #object-examples button {
125
- height: auto !important;
126
- min-height: 36px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  overflow-wrap: anywhere;
128
- text-align: left;
129
- white-space: normal !important;
130
  }
131
 
132
- @media (max-width: 640px) {
133
- .gradio-container *,
134
- .gradio-container *::before,
135
- .gradio-container *::after {
136
- box-sizing: border-box;
137
- max-width: 100%;
138
- min-width: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  }
140
 
141
  .gradio-container {
142
- padding-left: 12px !important;
143
- padding-right: 12px !important;
144
- width: 100vw !important;
 
 
 
 
 
 
 
 
145
  }
146
 
147
  #objectverse-hero {
148
- box-sizing: border-box;
149
- padding: 18px;
150
- width: 100%;
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
  #objectverse-hero h1 {
154
- font-size: 28px;
155
  overflow-wrap: anywhere;
156
  }
157
 
158
- #objectverse-hero p {
159
- font-size: 14px;
160
- overflow-wrap: anywhere;
161
- white-space: normal;
 
 
 
 
 
 
 
 
 
 
162
  }
163
 
164
  .archive-panel {
165
- box-sizing: border-box;
166
- padding: 12px;
167
- width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
  }
 
1
+ :root {
2
+ --ov-bg: #15120e;
3
+ --ov-panel: rgba(31, 27, 21, 0.9);
4
+ --ov-panel-soft: rgba(44, 37, 28, 0.72);
5
+ --ov-border: rgba(214, 165, 82, 0.34);
6
+ --ov-border-strong: rgba(232, 176, 82, 0.58);
7
+ --ov-text: #f0e4d0;
8
+ --ov-muted: #c4ad8d;
9
+ --ov-faint: #8f7b5f;
10
+ --ov-amber: #d99a35;
11
+ --ov-amber-bright: #f0bd62;
12
+ --ov-green: #9fb37a;
13
+ --ov-red: #b96f55;
14
+ --ov-shadow: 0 18px 50px rgba(0, 0, 0, 0.38);
15
+ }
16
+
17
+ html,
18
+ body,
19
+ gradio-app {
20
+ background: var(--ov-bg);
21
  overflow-x: hidden;
22
+ width: 100%;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ }
28
+
29
+ .gradio-container,
30
+ .gradio-container * {
31
+ box-sizing: border-box;
32
+ min-width: 0;
33
  }
34
 
35
  .gradio-container {
36
  background:
37
+ linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
38
+ linear-gradient(90deg, rgba(255, 255, 255, 0.014) 1px, transparent 1px),
39
+ linear-gradient(135deg, #18140f 0%, #241d16 45%, #11100e 100%);
40
+ background-size: 28px 28px, 28px 28px, auto;
41
+ color: var(--ov-text);
42
  font-family: Georgia, "Times New Roman", serif;
43
+ margin: 0 auto !important;
44
+ max-width: 1480px !important;
45
+ min-height: 100vh;
46
  overflow-x: hidden;
47
+ padding: 24px !important;
48
+ }
49
+
50
+ .gradio-container > main,
51
+ .gradio-container > main > .wrap,
52
+ .gradio-container > main > .wrap > .contain {
53
+ margin-left: 0 !important;
54
+ margin-right: 0 !important;
55
+ max-width: 100% !important;
56
+ padding-left: 0 !important;
57
+ padding-right: 0 !important;
58
+ width: 100% !important;
59
+ }
60
+
61
+ .gradio-container .contain {
62
+ max-width: none !important;
63
  }
64
 
65
  #objectverse-hero {
66
+ align-items: center;
67
+ background:
68
+ linear-gradient(90deg, rgba(217, 154, 53, 0.08), transparent 34%),
69
+ rgba(23, 20, 16, 0.86);
70
+ border: 1px solid var(--ov-border);
71
+ border-radius: 8px;
72
+ box-shadow: var(--ov-shadow);
73
+ display: grid;
74
+ gap: 18px;
75
+ grid-template-columns: auto 1fr auto;
76
+ padding: 18px;
77
+ }
78
+
79
+ .hero-mark {
80
+ align-items: center;
81
+ border: 1px solid var(--ov-border-strong);
82
+ border-radius: 50%;
83
+ color: var(--ov-amber-bright);
84
+ display: flex;
85
+ flex-direction: column;
86
+ height: 82px;
87
+ justify-content: center;
88
+ width: 82px;
89
+ }
90
+
91
+ .hero-mark span,
92
+ .hero-mark small {
93
+ letter-spacing: 0;
94
+ }
95
+
96
+ .hero-mark span {
97
+ font-size: 20px;
98
+ }
99
+
100
+ .hero-mark small {
101
+ color: var(--ov-muted);
102
+ font-size: 11px;
103
+ }
104
+
105
+ .hero-kicker {
106
+ color: var(--ov-amber-bright);
107
+ font-size: 13px;
108
+ font-style: italic;
109
+ margin: 0 0 8px;
110
+ overflow-wrap: anywhere;
111
+ white-space: normal;
112
  }
113
 
114
  #objectverse-hero h1 {
115
+ color: var(--ov-text);
116
+ font-size: 40px;
117
+ line-height: 1.05;
118
  margin: 0 0 8px;
119
  }
120
 
121
  #objectverse-hero p {
122
+ color: var(--ov-muted);
123
  font-size: 16px;
124
+ line-height: 1.5;
125
  margin: 0;
126
+ overflow-wrap: anywhere;
127
+ white-space: normal;
128
+ }
129
+
130
+ #objectverse-hero .hero-copy span {
131
+ color: var(--ov-muted);
132
+ }
133
+
134
+ .hero-badges {
135
+ display: flex;
136
+ flex-wrap: wrap;
137
+ gap: 10px;
138
+ justify-content: flex-end;
139
+ }
140
+
141
+ #objectverse-hero .hero-badges span {
142
+ border: 1px solid var(--ov-border);
143
+ border-radius: 6px;
144
+ color: var(--ov-amber-bright);
145
+ font-size: 13px;
146
+ padding: 10px 14px;
147
+ white-space: nowrap;
148
+ }
149
+
150
+ #objectverse-hero .hero-mark span {
151
+ color: var(--ov-amber-bright);
152
  }
153
 
154
+ #archive-main-grid,
155
+ #archive-bottom-grid {
156
+ gap: 16px;
157
+ margin-top: 16px;
158
  }
159
 
160
  .archive-panel {
161
+ background:
162
+ linear-gradient(rgba(255, 255, 255, 0.025), transparent),
163
+ var(--ov-panel);
164
+ border: 1px solid var(--ov-border);
165
+ border-radius: 8px;
166
+ box-shadow: var(--ov-shadow);
167
+ padding: 16px;
168
  }
169
 
170
+ .archive-panel .block,
171
+ .archive-panel .form,
172
+ .archive-panel .wrap,
173
+ .archive-panel .input-container,
174
+ .archive-panel textarea,
175
+ .archive-panel input {
176
+ background-color: transparent !important;
177
+ color: var(--ov-text) !important;
178
  }
179
 
180
+ .panel-header {
181
+ align-items: flex-start;
182
+ border-bottom: 1px solid rgba(214, 165, 82, 0.18);
183
+ display: flex;
184
+ gap: 12px;
185
+ margin-bottom: 16px;
186
+ padding-bottom: 12px;
187
  }
188
 
189
+ .panel-header > span {
190
+ background: rgba(217, 154, 53, 0.16);
191
+ border: 1px solid var(--ov-border);
192
+ border-radius: 6px;
193
+ color: var(--ov-amber-bright) !important;
194
+ display: inline-flex;
195
+ flex: 0 0 auto;
196
+ font-size: 13px;
197
+ justify-content: center;
198
+ padding: 7px 9px;
199
  }
200
 
201
+ .panel-header h2 {
202
+ color: var(--ov-text) !important;
203
+ font-size: 19px;
204
+ line-height: 1.2;
205
+ margin: 0;
206
+ }
207
+
208
+ .panel-header small {
209
+ color: var(--ov-muted) !important;
210
+ font-size: 13px;
211
+ font-weight: 400;
212
+ }
213
+
214
+ .panel-header p {
215
+ color: var(--ov-muted) !important;
216
+ font-size: 13px;
217
+ line-height: 1.45;
218
+ margin: 5px 0 0;
219
+ }
220
+
221
+ .gradio-container label,
222
+ .gradio-container .label-wrap span {
223
+ color: var(--ov-muted) !important;
224
+ }
225
+
226
+ .gradio-container textarea,
227
+ .gradio-container input[type="text"] {
228
+ border: 1px solid rgba(214, 165, 82, 0.24) !important;
229
+ border-radius: 6px !important;
230
+ color: var(--ov-text) !important;
231
+ font-family: Georgia, "Times New Roman", serif !important;
232
+ line-height: 1.5 !important;
233
+ overflow-wrap: break-word !important;
234
+ white-space: pre-wrap !important;
235
+ }
236
+
237
+ .gradio-container textarea::placeholder,
238
+ .gradio-container input::placeholder {
239
+ color: rgba(196, 173, 141, 0.68) !important;
240
+ }
241
+
242
+ #object-upload {
243
+ border: 1px dashed rgba(217, 154, 53, 0.52);
244
+ border-radius: 8px;
245
+ overflow: hidden;
246
+ }
247
+
248
+ #personality-mode .wrap,
249
+ .mode-switch .wrap {
250
+ display: flex !important;
251
+ flex-wrap: wrap !important;
252
+ gap: 8px !important;
253
+ }
254
+
255
+ #personality-mode label,
256
+ .mode-switch label {
257
+ align-items: center;
258
+ background: rgba(31, 27, 21, 0.92) !important;
259
+ border: 1px solid rgba(214, 165, 82, 0.34) !important;
260
+ border-radius: 7px !important;
261
+ color: var(--ov-muted) !important;
262
+ display: flex !important;
263
+ flex: 1 1 102px;
264
+ justify-content: center;
265
+ min-height: 48px;
266
+ padding: 8px !important;
267
+ text-align: center;
268
+ white-space: normal !important;
269
+ }
270
+
271
+ #personality-mode label span,
272
+ .mode-switch label span {
273
+ color: var(--ov-muted) !important;
274
+ overflow-wrap: anywhere;
275
+ white-space: normal !important;
276
+ }
277
+
278
+ #personality-mode label:has(input:checked),
279
+ .mode-switch label:has(input:checked) {
280
+ background: rgba(217, 154, 53, 0.18) !important;
281
+ border-color: var(--ov-amber) !important;
282
+ color: var(--ov-amber-bright) !important;
283
+ }
284
+
285
+ #personality-mode label:has(input:checked) span,
286
+ .mode-switch label:has(input:checked) span {
287
+ color: var(--ov-amber-bright) !important;
288
+ }
289
+
290
+ #wake-button {
291
+ background: linear-gradient(180deg, #e0ad62 0%, #bd7926 100%) !important;
292
+ border: 1px solid #f0bd62 !important;
293
+ border-radius: 8px !important;
294
+ color: #1d140b !important;
295
+ font-family: Georgia, "Times New Roman", serif !important;
296
+ font-size: 18px !important;
297
+ font-weight: 700 !important;
298
+ min-height: 58px;
299
+ text-shadow: none !important;
300
+ }
301
+
302
+ .quiet-button {
303
+ border: 1px solid var(--ov-border) !important;
304
+ color: var(--ov-amber-bright) !important;
305
+ }
306
+
307
+ .example-section-title {
308
+ align-items: baseline;
309
+ border-top: 1px solid rgba(214, 165, 82, 0.18);
310
+ display: flex;
311
+ gap: 10px;
312
+ justify-content: space-between;
313
+ margin: 18px 0 10px;
314
+ padding-top: 14px;
315
+ }
316
+
317
+ .example-section-title span {
318
+ color: var(--ov-text);
319
+ font-size: 15px;
320
+ }
321
+
322
+ .example-section-title small {
323
+ color: var(--ov-faint);
324
+ font-size: 12px;
325
+ }
326
+
327
+ button.example-card {
328
+ background:
329
+ linear-gradient(90deg, rgba(217, 154, 53, 0.1), transparent 52%),
330
+ var(--ov-panel-soft) !important;
331
+ border: 1px solid rgba(214, 165, 82, 0.26) !important;
332
+ border-radius: 7px !important;
333
+ color: var(--ov-text) !important;
334
+ display: block !important;
335
+ font-family: Georgia, "Times New Roman", serif !important;
336
+ height: auto !important;
337
+ line-height: 1.4 !important;
338
+ margin-top: 8px !important;
339
+ min-height: 78px;
340
+ overflow-wrap: anywhere;
341
+ padding: 12px !important;
342
+ text-align: left !important;
343
+ white-space: pre-wrap !important;
344
+ width: 100%;
345
+ }
346
+
347
+ button.example-card:hover,
348
+ .quiet-button:hover {
349
+ border-color: var(--ov-amber) !important;
350
+ color: var(--ov-amber-bright) !important;
351
+ }
352
+
353
+ .archive-empty,
354
+ .objectverse-placeholder,
355
+ .archive-error {
356
+ border: 1px dashed rgba(214, 165, 82, 0.3);
357
+ border-radius: 8px;
358
+ color: var(--ov-muted) !important;
359
  line-height: 1.55;
360
+ padding: 18px;
 
361
  }
362
 
363
+ .archive-empty h3,
364
+ .objectverse-placeholder strong,
365
+ .archive-error strong {
366
+ color: var(--ov-text) !important;
367
+ display: block;
368
+ font-size: 20px;
369
+ margin: 8px 0;
370
+ }
371
+
372
+ .archive-empty.compact,
373
+ .trace-card {
374
+ padding: 14px;
375
+ }
376
+
377
+ .archive-label,
378
+ .objectverse-placeholder span,
379
+ .archive-error span {
380
+ color: var(--ov-amber-bright) !important;
381
+ display: block;
382
+ font-size: 12px;
383
+ text-transform: uppercase;
384
+ }
385
+
386
+ .archive-error {
387
+ border-color: rgba(185, 111, 85, 0.72);
388
+ }
389
+
390
+ .archive-error span,
391
+ .archive-error strong {
392
+ color: #f3a184 !important;
393
+ }
394
+
395
+ .archive-empty p,
396
+ .objectverse-placeholder p,
397
+ .archive-error p {
398
+ color: var(--ov-muted) !important;
399
+ }
400
+
401
+ .object-file-card,
402
+ .trace-card {
403
+ background: rgba(18, 16, 13, 0.52);
404
+ border: 1px solid rgba(214, 165, 82, 0.24);
405
+ border-radius: 8px;
406
+ padding: 18px;
407
+ }
408
+
409
+ .file-meta {
410
  display: flex;
411
  flex-wrap: wrap;
412
  gap: 8px;
413
+ margin-bottom: 12px;
414
  }
415
 
416
+ .file-meta span,
417
+ .file-tags span,
418
  .card-tags span {
419
+ border: 1px solid rgba(214, 165, 82, 0.28);
420
+ border-radius: 999px;
421
+ color: var(--ov-amber-bright);
422
+ display: inline-flex;
423
+ font-size: 12px;
424
+ line-height: 1;
425
+ padding: 7px 9px;
426
+ }
427
+
428
+ .object-file-card h3 {
429
+ color: var(--ov-text);
430
+ font-size: 28px;
431
+ line-height: 1.12;
432
+ margin: 0 0 8px;
433
+ }
434
+
435
+ .object-name {
436
+ color: var(--ov-muted);
437
+ margin: 0 0 16px;
438
+ }
439
+
440
+ .object-file-card dl {
441
+ display: grid;
442
+ gap: 10px;
443
+ margin: 0 0 16px;
444
  }
445
 
446
+ .object-file-card dl > div {
447
+ border-top: 1px solid rgba(214, 165, 82, 0.14);
448
+ padding-top: 10px;
449
+ }
450
+
451
+ .object-file-card dt {
452
+ color: var(--ov-faint);
453
+ font-size: 12px;
454
+ margin-bottom: 3px;
455
+ text-transform: uppercase;
456
+ }
457
+
458
+ .object-file-card dd {
459
+ color: var(--ov-text);
460
+ line-height: 1.45;
461
+ margin: 0;
462
+ }
463
+
464
+ .feature-list {
465
+ border: 1px solid rgba(159, 179, 122, 0.25);
466
+ border-radius: 7px;
467
+ margin-bottom: 16px;
468
+ padding: 12px 14px;
469
+ }
470
+
471
+ .feature-list strong {
472
+ color: var(--ov-green);
473
+ }
474
+
475
+ .feature-list ul {
476
+ color: var(--ov-muted);
477
+ margin: 8px 0 0;
478
+ padding-left: 18px;
479
+ }
480
+
481
+ .complaint {
482
+ border-left: 3px solid var(--ov-red);
483
+ color: var(--ov-text);
484
+ font-style: italic;
485
  line-height: 1.55;
486
+ margin: 0 0 14px;
487
+ padding-left: 12px;
488
+ }
489
+
490
+ .file-tags,
491
+ .card-tags {
492
+ display: flex;
493
+ flex-wrap: wrap;
494
+ gap: 8px;
495
  }
496
 
497
  #diary-output,
498
  #diary-output * {
499
+ color: var(--ov-muted) !important;
500
+ }
501
+
502
+ #diary-output {
503
+ background: rgba(18, 16, 13, 0.5);
504
+ border: 1px solid rgba(214, 165, 82, 0.22);
505
+ border-radius: 8px;
506
+ min-height: 320px;
507
+ padding: 18px !important;
508
  }
509
 
510
  #diary-output h1,
511
  #diary-output h2,
512
  #diary-output h3 {
513
+ color: var(--ov-amber-bright) !important;
514
+ }
515
+
516
+ #diary-output p {
517
+ font-size: 16px;
518
+ line-height: 1.7;
519
  }
520
 
521
+ .objectverse-card {
522
+ background:
523
+ linear-gradient(180deg, rgba(255, 245, 218, 0.06), rgba(34, 24, 14, 0.1)),
524
+ #241b12;
525
+ border: 1px solid rgba(240, 189, 98, 0.58);
526
+ border-radius: 8px;
527
+ box-shadow: 0 22px 55px rgba(0, 0, 0, 0.45);
528
+ color: var(--ov-text);
529
+ max-width: 560px;
530
+ padding: 24px;
531
  width: 100%;
532
  }
533
 
534
+ .card-header {
535
+ align-items: flex-start;
536
+ display: flex;
537
+ gap: 12px;
538
+ justify-content: space-between;
539
  }
540
 
541
+ .objectverse-card h2 {
542
+ color: var(--ov-text);
543
+ font-size: 32px;
544
+ line-height: 1.08;
545
+ margin: 8px 0;
 
546
  }
547
 
548
+ .card-kicker,
549
+ .card-object,
550
+ .card-cn {
551
+ color: var(--ov-muted);
552
+ letter-spacing: 0;
553
+ }
554
+
555
+ .card-kicker {
556
+ font-size: 12px;
557
+ text-transform: uppercase;
558
+ }
559
+
560
+ .card-stamp {
561
+ border: 1px solid rgba(217, 154, 53, 0.42);
562
+ border-radius: 50%;
563
+ color: var(--ov-amber-bright);
564
+ flex: 0 0 auto;
565
+ font-size: 11px;
566
+ height: 64px;
567
+ padding-top: 24px;
568
+ text-align: center;
569
+ width: 64px;
570
+ }
571
+
572
+ .card-quote {
573
+ border-left: 3px solid var(--ov-amber);
574
+ color: var(--ov-text);
575
+ font-size: 18px;
576
+ line-height: 1.62;
577
+ margin: 20px 0 14px;
578
+ padding-left: 14px;
579
+ }
580
+
581
+ .card-cn {
582
+ font-size: 14px;
583
+ line-height: 1.6;
584
+ margin: 0 0 18px;
585
+ }
586
+
587
+ .trace-card strong {
588
+ color: var(--ov-text);
589
+ display: block;
590
+ margin: 8px 0;
591
  overflow-wrap: anywhere;
 
 
592
  }
593
 
594
+ .trace-card p {
595
+ color: var(--ov-muted);
596
+ line-height: 1.5;
597
+ margin: 0;
598
+ }
599
+
600
+ .gradio-container .json-holder,
601
+ .gradio-container pre {
602
+ max-width: 100%;
603
+ overflow: auto !important;
604
+ }
605
+
606
+ @media (max-width: 980px) {
607
+ gradio-app,
608
+ .gradio-container,
609
+ .gradio-container .main,
610
+ .gradio-container .contain {
611
+ margin: 0 !important;
612
+ max-width: 100vw !important;
613
+ overflow-x: hidden !important;
614
+ width: 100vw !important;
615
  }
616
 
617
  .gradio-container {
618
+ padding: 14px !important;
619
+ }
620
+
621
+ .gradio-container > main,
622
+ .gradio-container > main > .wrap,
623
+ .gradio-container > main > .wrap > .contain {
624
+ max-width: 100% !important;
625
+ overflow-x: hidden !important;
626
+ padding-left: 0 !important;
627
+ padding-right: 0 !important;
628
+ width: 100% !important;
629
  }
630
 
631
  #objectverse-hero {
632
+ grid-template-columns: 1fr;
633
+ max-width: calc(100vw - 28px);
634
+ width: calc(100vw - 28px);
635
+ }
636
+
637
+ #archive-main-grid,
638
+ #archive-bottom-grid {
639
+ max-width: calc(100vw - 28px);
640
+ width: calc(100vw - 28px);
641
+ }
642
+
643
+ .hero-mark {
644
+ height: 68px;
645
+ width: 68px;
646
  }
647
 
648
  #objectverse-hero h1 {
649
+ font-size: 32px;
650
  overflow-wrap: anywhere;
651
  }
652
 
653
+ .hero-badges {
654
+ justify-content: flex-start;
655
+ }
656
+
657
+ .hero-badges span {
658
+ flex: 1 1 100%;
659
+ text-align: center;
660
+ }
661
+
662
+ #archive-main-grid,
663
+ #archive-bottom-grid,
664
+ .gradio-container .gr-row {
665
+ flex-direction: column !important;
666
+ gap: 14px !important;
667
  }
668
 
669
  .archive-panel {
670
+ padding: 14px;
671
+ width: 100% !important;
672
+ }
673
+
674
+ #personality-mode label,
675
+ .mode-switch label {
676
+ flex-basis: 120px;
677
+ }
678
+
679
+ .example-section-title {
680
+ align-items: flex-start;
681
+ flex-direction: column;
682
+ gap: 4px;
683
+ }
684
+
685
+ #diary-output {
686
+ min-height: 240px;
687
+ }
688
+
689
+ .objectverse-card {
690
+ max-width: 100%;
691
+ }
692
+ }
693
+
694
+ @media (max-width: 430px) {
695
+ .gradio-container {
696
+ padding-left: 10px !important;
697
+ padding-right: 10px !important;
698
+ }
699
+
700
+ #objectverse-hero,
701
+ .archive-panel {
702
+ border-radius: 7px;
703
+ }
704
+
705
+ .panel-header h2 {
706
+ font-size: 17px;
707
+ }
708
+
709
+ .panel-header {
710
+ gap: 9px;
711
+ }
712
+
713
+ #personality-mode label,
714
+ .mode-switch label {
715
+ flex-basis: 100%;
716
+ }
717
+
718
+ .object-file-card h3,
719
+ .objectverse-card h2 {
720
+ font-size: 25px;
721
+ }
722
+
723
+ .card-header {
724
+ flex-direction: column;
725
+ }
726
+
727
+ .card-stamp {
728
+ border-radius: 999px;
729
+ height: auto;
730
+ padding: 7px 10px;
731
+ width: auto;
732
  }
733
  }