caudate-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
api/artifact_viewer.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Artifact viewer — polished single-page renderer for FileStore items.
|
|
2
|
+
|
|
3
|
+
Phase 1 of `COGNOS_UI_ROADMAP.md`. The route `GET /artifact/{file_id}`
|
|
4
|
+
serves a clean HTML page that renders the file according to its type:
|
|
5
|
+
|
|
6
|
+
- HTML / SVG → iframe / inline preview + collapsible source view
|
|
7
|
+
- Code (py/js/ts/html/css/md/json/yaml/sh/...) → syntax-highlighted
|
|
8
|
+
- Markdown → rendered (marked.js) + raw toggle
|
|
9
|
+
- Image → full-size view + download
|
|
10
|
+
- Audio → HTML5 player + download
|
|
11
|
+
- Other → download-only fallback
|
|
12
|
+
|
|
13
|
+
Compared to the raw `/files/{id}/content` endpoint, this route gives
|
|
14
|
+
each artifact its own polished standalone page — the foundation for
|
|
15
|
+
later moving toward a side-pane experience inside the chat UI.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import html
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Map file suffixes to (kind, prism-language) — drives the renderer.
|
|
27
|
+
# `kind` is one of: html, svg, image, audio, video, markdown, code, text, binary.
|
|
28
|
+
# `prism` is the Prism.js language tag used when the kind is `code` or `markdown` raw.
|
|
29
|
+
_TYPE_MAP: dict[str, tuple[str, str]] = {
|
|
30
|
+
".html": ("html", "markup"),
|
|
31
|
+
".htm": ("html", "markup"),
|
|
32
|
+
".svg": ("svg", "markup"),
|
|
33
|
+
".md": ("markdown", "markdown"),
|
|
34
|
+
".markdown": ("markdown", "markdown"),
|
|
35
|
+
".py": ("code", "python"),
|
|
36
|
+
".js": ("code", "javascript"),
|
|
37
|
+
".mjs": ("code", "javascript"),
|
|
38
|
+
".ts": ("code", "typescript"),
|
|
39
|
+
".tsx": ("code", "tsx"),
|
|
40
|
+
".jsx": ("code", "jsx"),
|
|
41
|
+
".css": ("code", "css"),
|
|
42
|
+
".json": ("code", "json"),
|
|
43
|
+
".yaml": ("code", "yaml"),
|
|
44
|
+
".yml": ("code", "yaml"),
|
|
45
|
+
".toml": ("code", "toml"),
|
|
46
|
+
".sh": ("code", "bash"),
|
|
47
|
+
".bash": ("code", "bash"),
|
|
48
|
+
".rs": ("code", "rust"),
|
|
49
|
+
".go": ("code", "go"),
|
|
50
|
+
".java": ("code", "java"),
|
|
51
|
+
".c": ("code", "c"),
|
|
52
|
+
".cpp": ("code", "cpp"),
|
|
53
|
+
".sql": ("code", "sql"),
|
|
54
|
+
".xml": ("code", "xml"),
|
|
55
|
+
".txt": ("text", "none"),
|
|
56
|
+
".log": ("text", "none"),
|
|
57
|
+
".png": ("image", "none"),
|
|
58
|
+
".jpg": ("image", "none"),
|
|
59
|
+
".jpeg": ("image", "none"),
|
|
60
|
+
".webp": ("image", "none"),
|
|
61
|
+
".gif": ("image", "none"),
|
|
62
|
+
".bmp": ("image", "none"),
|
|
63
|
+
".wav": ("audio", "none"),
|
|
64
|
+
".mp3": ("audio", "none"),
|
|
65
|
+
".ogg": ("audio", "none"),
|
|
66
|
+
".flac": ("audio", "none"),
|
|
67
|
+
".m4a": ("audio", "none"),
|
|
68
|
+
".mp4": ("video", "none"),
|
|
69
|
+
".webm": ("video", "none"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _classify(filename: str, mime_type: str) -> tuple[str, str]:
|
|
74
|
+
"""Return (kind, prism-language) for the file."""
|
|
75
|
+
suffix = Path(filename).suffix.lower()
|
|
76
|
+
if suffix in _TYPE_MAP:
|
|
77
|
+
return _TYPE_MAP[suffix]
|
|
78
|
+
# Fallback by MIME family
|
|
79
|
+
if mime_type:
|
|
80
|
+
if mime_type.startswith("image/"):
|
|
81
|
+
return ("image", "none")
|
|
82
|
+
if mime_type.startswith("audio/"):
|
|
83
|
+
return ("audio", "none")
|
|
84
|
+
if mime_type.startswith("video/"):
|
|
85
|
+
return ("video", "none")
|
|
86
|
+
if mime_type.startswith("text/"):
|
|
87
|
+
return ("text", "none")
|
|
88
|
+
if "json" in mime_type:
|
|
89
|
+
return ("code", "json")
|
|
90
|
+
if "html" in mime_type:
|
|
91
|
+
return ("html", "markup")
|
|
92
|
+
if "svg" in mime_type:
|
|
93
|
+
return ("svg", "markup")
|
|
94
|
+
return ("binary", "none")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def render_artifact_page(
|
|
98
|
+
*,
|
|
99
|
+
file_id: str,
|
|
100
|
+
filename: str,
|
|
101
|
+
mime_type: str,
|
|
102
|
+
size_bytes: int,
|
|
103
|
+
content_bytes: bytes,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Build the single-page viewer HTML for one artifact."""
|
|
106
|
+
kind, prism = _classify(filename, mime_type)
|
|
107
|
+
|
|
108
|
+
# The /files/<id>/content URL — used by iframes / image / audio
|
|
109
|
+
raw_url = f"/files/{file_id}/content"
|
|
110
|
+
|
|
111
|
+
# For text-y kinds, decode and stash raw text for source view + JS render
|
|
112
|
+
raw_text = ""
|
|
113
|
+
if kind in ("html", "svg", "code", "markdown", "text"):
|
|
114
|
+
try:
|
|
115
|
+
raw_text = content_bytes.decode("utf-8", errors="replace")
|
|
116
|
+
except Exception:
|
|
117
|
+
raw_text = ""
|
|
118
|
+
|
|
119
|
+
safe_filename = html.escape(filename)
|
|
120
|
+
safe_size = _humanize_bytes(size_bytes)
|
|
121
|
+
type_badge = _badge_for_kind(kind, prism)
|
|
122
|
+
|
|
123
|
+
# Body section depends on kind
|
|
124
|
+
body = _render_body(kind, prism, raw_url, raw_text, filename)
|
|
125
|
+
|
|
126
|
+
# Embed raw text in a JSON-encoded JS variable so toggles can read
|
|
127
|
+
# it without a second fetch. Limit size to avoid huge pages —
|
|
128
|
+
# >2 MB falls back to "use the download button".
|
|
129
|
+
embedded_text = json.dumps(raw_text) if len(raw_text) < 2_000_000 else "null"
|
|
130
|
+
|
|
131
|
+
return f"""<!DOCTYPE html>
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="UTF-8">
|
|
135
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
136
|
+
<title>{safe_filename} — Cognos artifact</title>
|
|
137
|
+
<link rel="stylesheet"
|
|
138
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
|
139
|
+
<style>
|
|
140
|
+
:root {{
|
|
141
|
+
--bg: #0f1115;
|
|
142
|
+
--bg-elev: #181b21;
|
|
143
|
+
--bg-pre: #0c0e12;
|
|
144
|
+
--text: #e6e7eb;
|
|
145
|
+
--text-dim: #8a8e98;
|
|
146
|
+
--border: #262a32;
|
|
147
|
+
--accent: #7aa2f7;
|
|
148
|
+
}}
|
|
149
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
150
|
+
html, body {{ height: 100%; }}
|
|
151
|
+
body {{
|
|
152
|
+
background: var(--bg);
|
|
153
|
+
color: var(--text);
|
|
154
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
line-height: 1.55;
|
|
157
|
+
display: flex; flex-direction: column;
|
|
158
|
+
}}
|
|
159
|
+
header {{
|
|
160
|
+
display: flex; align-items: center; gap: 12px;
|
|
161
|
+
padding: 10px 16px;
|
|
162
|
+
background: var(--bg-elev);
|
|
163
|
+
border-bottom: 1px solid var(--border);
|
|
164
|
+
}}
|
|
165
|
+
header .filename {{ font-weight: 600; font-size: 13px; }}
|
|
166
|
+
header .meta {{ color: var(--text-dim); font-size: 11px; }}
|
|
167
|
+
header .badge {{
|
|
168
|
+
background: var(--bg);
|
|
169
|
+
border: 1px solid var(--border);
|
|
170
|
+
padding: 2px 8px; border-radius: 10px;
|
|
171
|
+
font-size: 11px; color: var(--accent);
|
|
172
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
173
|
+
}}
|
|
174
|
+
header .controls {{ margin-left: auto; display: flex; gap: 6px; }}
|
|
175
|
+
button, .btn {{
|
|
176
|
+
background: var(--bg);
|
|
177
|
+
color: var(--text);
|
|
178
|
+
border: 1px solid var(--border);
|
|
179
|
+
padding: 6px 12px;
|
|
180
|
+
font-size: 12px; cursor: pointer;
|
|
181
|
+
border-radius: 4px; text-decoration: none;
|
|
182
|
+
font-family: inherit;
|
|
183
|
+
}}
|
|
184
|
+
button:hover, .btn:hover {{ border-color: var(--accent); color: var(--accent); }}
|
|
185
|
+
main {{
|
|
186
|
+
flex: 1; min-height: 0; overflow: auto;
|
|
187
|
+
padding: 16px 22px;
|
|
188
|
+
}}
|
|
189
|
+
iframe.preview {{
|
|
190
|
+
width: 100%; height: calc(100vh - 110px);
|
|
191
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
192
|
+
background: white;
|
|
193
|
+
}}
|
|
194
|
+
img.preview {{ max-width: 100%; max-height: calc(100vh - 110px); border-radius: 6px; }}
|
|
195
|
+
audio.preview, video.preview {{ width: 100%; }}
|
|
196
|
+
.svg-container {{ background: white; padding: 20px; border-radius: 6px;
|
|
197
|
+
max-width: 100%; overflow: auto; }}
|
|
198
|
+
.svg-container svg {{ max-width: 100%; height: auto; }}
|
|
199
|
+
.markdown-rendered {{
|
|
200
|
+
background: var(--bg-elev); border: 1px solid var(--border);
|
|
201
|
+
padding: 22px 28px; border-radius: 6px; max-width: 760px;
|
|
202
|
+
}}
|
|
203
|
+
.markdown-rendered h1 {{ margin: 0 0 14px; font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }}
|
|
204
|
+
.markdown-rendered h2 {{ margin: 18px 0 10px; font-size: 17px; }}
|
|
205
|
+
.markdown-rendered h3 {{ margin: 14px 0 8px; font-size: 14px; color: var(--text-dim); }}
|
|
206
|
+
.markdown-rendered p, .markdown-rendered li {{ margin: 4px 0 8px; }}
|
|
207
|
+
.markdown-rendered ul, .markdown-rendered ol {{ padding-left: 24px; }}
|
|
208
|
+
.markdown-rendered code {{ background: var(--bg-pre); padding: 1px 5px; border-radius: 3px; font-size: 12px; }}
|
|
209
|
+
.markdown-rendered pre {{ background: var(--bg-pre); padding: 12px; border-radius: 4px; overflow-x: auto; }}
|
|
210
|
+
.markdown-rendered pre code {{ background: transparent; padding: 0; font-size: 12px; }}
|
|
211
|
+
.markdown-rendered table {{ border-collapse: collapse; margin: 8px 0; }}
|
|
212
|
+
.markdown-rendered th, .markdown-rendered td {{ border: 1px solid var(--border); padding: 4px 8px; }}
|
|
213
|
+
.markdown-rendered a {{ color: var(--accent); }}
|
|
214
|
+
pre.source {{
|
|
215
|
+
background: var(--bg-pre); border: 1px solid var(--border);
|
|
216
|
+
border-radius: 4px; padding: 14px;
|
|
217
|
+
overflow-x: auto; max-height: calc(100vh - 110px);
|
|
218
|
+
font-size: 12.5px;
|
|
219
|
+
}}
|
|
220
|
+
.text-content {{
|
|
221
|
+
background: var(--bg-elev); border: 1px solid var(--border);
|
|
222
|
+
padding: 14px; border-radius: 4px;
|
|
223
|
+
white-space: pre-wrap; word-wrap: break-word;
|
|
224
|
+
font-family: ui-monospace, "SF Mono", Consolas, monospace;
|
|
225
|
+
font-size: 12.5px;
|
|
226
|
+
}}
|
|
227
|
+
.hidden {{ display: none !important; }}
|
|
228
|
+
.empty {{ color: var(--text-dim); padding: 20px; text-align: center; font-style: italic; }}
|
|
229
|
+
</style>
|
|
230
|
+
</head>
|
|
231
|
+
<body>
|
|
232
|
+
<header>
|
|
233
|
+
<span class="filename">{safe_filename}</span>
|
|
234
|
+
<span class="badge">{type_badge}</span>
|
|
235
|
+
<span class="meta">{safe_size}</span>
|
|
236
|
+
<div class="controls">
|
|
237
|
+
{_render_view_toggle(kind)}
|
|
238
|
+
<a class="btn" href="{raw_url}" download="{safe_filename}">Download</a>
|
|
239
|
+
</div>
|
|
240
|
+
</header>
|
|
241
|
+
<main>
|
|
242
|
+
{body}
|
|
243
|
+
</main>
|
|
244
|
+
<script>
|
|
245
|
+
window.__ARTIFACT__ = {{
|
|
246
|
+
kind: {json.dumps(kind)},
|
|
247
|
+
filename: {json.dumps(filename)},
|
|
248
|
+
rawText: {embedded_text},
|
|
249
|
+
rawUrl: {json.dumps(raw_url)},
|
|
250
|
+
}};
|
|
251
|
+
{_VIEW_TOGGLE_JS}
|
|
252
|
+
</script>
|
|
253
|
+
{_RENDERER_SCRIPTS_FOR_KIND.get(kind, "")}
|
|
254
|
+
</body>
|
|
255
|
+
</html>"""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _humanize_bytes(n: int) -> str:
|
|
259
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
260
|
+
if n < 1024 or unit == "GB":
|
|
261
|
+
return f"{n:.0f}{unit}" if unit == "B" else f"{n:.1f}{unit}"
|
|
262
|
+
n /= 1024
|
|
263
|
+
return f"{n}TB"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _badge_for_kind(kind: str, prism: str) -> str:
|
|
267
|
+
if kind == "code":
|
|
268
|
+
return prism
|
|
269
|
+
return kind
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _render_view_toggle(kind: str) -> str:
|
|
273
|
+
"""Render the source/preview toggle button — only shown for kinds
|
|
274
|
+
that have a meaningful split between rendered preview and raw."""
|
|
275
|
+
if kind in ("html", "svg", "markdown"):
|
|
276
|
+
return '<button id="toggle-source-btn">View source</button>'
|
|
277
|
+
return ''
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _render_body(kind: str, prism: str, raw_url: str,
|
|
281
|
+
raw_text: str, filename: str) -> str:
|
|
282
|
+
"""Per-type body markup."""
|
|
283
|
+
safe_filename = html.escape(filename)
|
|
284
|
+
if kind == "html":
|
|
285
|
+
return f"""
|
|
286
|
+
<iframe id="preview-frame" class="preview" src="{raw_url}" sandbox="allow-scripts"></iframe>
|
|
287
|
+
<pre id="source-view" class="source hidden"><code class="language-markup">{html.escape(raw_text)}</code></pre>
|
|
288
|
+
"""
|
|
289
|
+
if kind == "svg":
|
|
290
|
+
return f"""
|
|
291
|
+
<div id="preview-frame" class="svg-container">{raw_text}</div>
|
|
292
|
+
<pre id="source-view" class="source hidden"><code class="language-markup">{html.escape(raw_text)}</code></pre>
|
|
293
|
+
"""
|
|
294
|
+
if kind == "markdown":
|
|
295
|
+
return f"""
|
|
296
|
+
<div id="preview-frame" class="markdown-rendered"></div>
|
|
297
|
+
<pre id="source-view" class="source hidden"><code class="language-markdown">{html.escape(raw_text)}</code></pre>
|
|
298
|
+
"""
|
|
299
|
+
if kind == "code":
|
|
300
|
+
return f"""
|
|
301
|
+
<pre class="source"><code class="language-{prism}">{html.escape(raw_text)}</code></pre>
|
|
302
|
+
"""
|
|
303
|
+
if kind == "text":
|
|
304
|
+
return f'<div class="text-content">{html.escape(raw_text)}</div>'
|
|
305
|
+
if kind == "image":
|
|
306
|
+
return f'<img class="preview" src="{raw_url}" alt="{safe_filename}">'
|
|
307
|
+
if kind == "audio":
|
|
308
|
+
return f'<audio class="preview" controls src="{raw_url}"></audio>'
|
|
309
|
+
if kind == "video":
|
|
310
|
+
return f'<video class="preview" controls src="{raw_url}"></video>'
|
|
311
|
+
# binary / unknown
|
|
312
|
+
return (
|
|
313
|
+
'<div class="empty">'
|
|
314
|
+
f'No inline preview for this file type. '
|
|
315
|
+
f'<a class="btn" href="{raw_url}" download="{safe_filename}">Download</a>'
|
|
316
|
+
'</div>'
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
_VIEW_TOGGLE_JS = r"""
|
|
321
|
+
(function() {
|
|
322
|
+
const btn = document.getElementById('toggle-source-btn');
|
|
323
|
+
const preview = document.getElementById('preview-frame');
|
|
324
|
+
const source = document.getElementById('source-view');
|
|
325
|
+
if (!btn || !preview || !source) return;
|
|
326
|
+
let showingSource = false;
|
|
327
|
+
btn.addEventListener('click', () => {
|
|
328
|
+
showingSource = !showingSource;
|
|
329
|
+
preview.classList.toggle('hidden', showingSource);
|
|
330
|
+
source.classList.toggle('hidden', !showingSource);
|
|
331
|
+
btn.textContent = showingSource ? 'View preview' : 'View source';
|
|
332
|
+
});
|
|
333
|
+
})();
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
# Per-kind external scripts (Prism + marked + initial render)
|
|
337
|
+
_RENDERER_SCRIPTS_FOR_KIND = {
|
|
338
|
+
"code": (
|
|
339
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>'
|
|
340
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>'
|
|
341
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>'
|
|
342
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>'
|
|
343
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>'
|
|
344
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>'
|
|
345
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-rust.min.js"></script>'
|
|
346
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script>'
|
|
347
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>'
|
|
348
|
+
),
|
|
349
|
+
"html": (
|
|
350
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>'
|
|
351
|
+
),
|
|
352
|
+
"svg": (
|
|
353
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>'
|
|
354
|
+
),
|
|
355
|
+
"markdown": (
|
|
356
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.2/marked.min.js"></script>'
|
|
357
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>'
|
|
358
|
+
'<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>'
|
|
359
|
+
'<script>'
|
|
360
|
+
' if (window.__ARTIFACT__.rawText && window.marked) {'
|
|
361
|
+
' document.getElementById("preview-frame").innerHTML = '
|
|
362
|
+
' window.marked.parse(window.__ARTIFACT__.rawText);'
|
|
363
|
+
' }'
|
|
364
|
+
'</script>'
|
|
365
|
+
),
|
|
366
|
+
}
|