cybercode-cli 1.0.0
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.
- package/LICENSE +53 -0
- package/README.md +174 -0
- package/bin/cli.mjs +314 -0
- package/package.json +44 -0
- package/python/agent_core.py +1029 -0
- package/python/webui_codex.html +673 -0
- package/python/webui_codex.py +583 -0
- package/skills/faceless-explainer.md +194 -0
- package/skills/general-video.md +141 -0
- package/skills/hyperframes-animation.md +82 -0
- package/skills/hyperframes-cli.md +109 -0
- package/skills/hyperframes-core.md +78 -0
- package/skills/hyperframes-creative.md +68 -0
- package/skills/hyperframes-media.md +81 -0
- package/skills/hyperframes-registry.md +101 -0
- package/skills/hyperframes.md +144 -0
- package/skills/motion-graphics.md +170 -0
- package/skills/product-launch-video.md +199 -0
- package/skills/website-to-video.md +141 -0
- package/templates/mykey_template.json +14 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>GenericAgent · Codex</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #2a2a2f;
|
|
10
|
+
--panel: #1f1f22;
|
|
11
|
+
--text: rgba(255, 255, 255, 0.94);
|
|
12
|
+
--muted: rgba(255, 255, 255, 0.54);
|
|
13
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
14
|
+
--accent: #6b8cff;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
|
|
19
|
+
html, body {
|
|
20
|
+
width: 100%; height: 100%; margin: 0; overflow: hidden;
|
|
21
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
22
|
+
background:
|
|
23
|
+
radial-gradient(circle at 17% 13%, rgba(214, 232, 255, 0.92) 0, rgba(164, 183, 255, 0.7) 9%, rgba(92, 112, 255, 0.12) 24%, rgba(92, 112, 255, 0) 34%),
|
|
24
|
+
radial-gradient(circle at 50% 1%, rgba(255, 255, 255, 0.9) 0, rgba(255, 255, 255, 0.55) 10%, rgba(255, 255, 255, 0) 26%),
|
|
25
|
+
radial-gradient(circle at 86% 45%, rgba(88, 58, 255, 0.62) 0, rgba(88, 58, 255, 0.3) 10%, rgba(88, 58, 255, 0) 28%),
|
|
26
|
+
radial-gradient(circle at 12% 92%, rgba(29, 82, 245, 0.92) 0, rgba(29, 82, 245, 0.52) 12%, rgba(29, 82, 245, 0) 34%),
|
|
27
|
+
radial-gradient(circle at 92% 93%, rgba(171, 123, 255, 0.82) 0, rgba(171, 123, 255, 0.42) 12%, rgba(171, 123, 255, 0) 35%),
|
|
28
|
+
linear-gradient(180deg, #6071ff 0%, #5d63f5 18%, #414fe5 44%, #1f3adf 72%, #2740f0 100%);
|
|
29
|
+
}
|
|
30
|
+
body::before {
|
|
31
|
+
content: ""; position: fixed; inset: -12%; pointer-events: none; opacity: 0.88; filter: blur(26px);
|
|
32
|
+
background:
|
|
33
|
+
radial-gradient(circle at 23% 22%, rgba(255,255,255,.18), transparent 22%),
|
|
34
|
+
radial-gradient(circle at 74% 26%, rgba(255,255,255,.11), transparent 20%),
|
|
35
|
+
radial-gradient(circle at 71% 78%, rgba(255,255,255,.08), transparent 18%);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.desktop { position: relative; width: 100vw; height: 100vh; }
|
|
39
|
+
|
|
40
|
+
.window {
|
|
41
|
+
position: absolute; left: 50%; top: 50%;
|
|
42
|
+
width: min(1413px, calc(100vw - 102px));
|
|
43
|
+
height: min(823px, calc(100vh - 138px));
|
|
44
|
+
transform: translate(-50%, -49.4%);
|
|
45
|
+
background: linear-gradient(180deg, #212123 0%, #202022 100%);
|
|
46
|
+
border-radius: 22px;
|
|
47
|
+
box-shadow: 0 24px 70px rgba(10,10,16,.28), 0 0 0 1px rgba(255,255,255,.03) inset;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ---- sidebar ---- */
|
|
52
|
+
.sidebar { position: absolute; left: 0; top: 0; bottom: 0; width: 285px; background: #19191b; border-right: 1px solid rgba(255,255,255,.02); display: flex; flex-direction: column; }
|
|
53
|
+
.sidebar-inner { padding: 14px 18px 14px 20px; color: var(--text); display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
54
|
+
.traffic { display: flex; gap: 8px; padding-top: 4px; margin-bottom: 22px; }
|
|
55
|
+
.traffic span { width: 12px; height: 12px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0,0,0,.08) inset; }
|
|
56
|
+
.traffic .red { background: #ff5f57; } .traffic .yellow { background: #febc2e; } .traffic .green { background: #28c840; }
|
|
57
|
+
|
|
58
|
+
.nav-item, .thread, .section-title { display: flex; align-items: center; gap: 10px; height: 30px; font-size: 14px; line-height: 1; }
|
|
59
|
+
.nav-item { color: rgba(255,255,255,.9); margin-bottom: 4px; cursor: pointer; border-radius: 8px; padding: 0 6px; }
|
|
60
|
+
.nav-item:hover { background: rgba(255,255,255,.05); }
|
|
61
|
+
.nav-item svg { flex: 0 0 auto; opacity: .9; }
|
|
62
|
+
.section-title { color: rgba(255,255,255,.5); margin: 16px 0 8px; font-size: 13px; height: 24px; letter-spacing: .02em; }
|
|
63
|
+
.scroll { flex: 1; overflow-y: auto; margin: 0 -6px; padding: 0 6px; }
|
|
64
|
+
.scroll::-webkit-scrollbar { width: 8px; } .scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 4px; }
|
|
65
|
+
|
|
66
|
+
.thread { color: rgba(255,255,255,.88); padding: 0 8px; margin: 1px 0; cursor: pointer; border-radius: 8px; }
|
|
67
|
+
.thread:hover { background: rgba(255,255,255,.06); }
|
|
68
|
+
.thread.active { background: rgba(107,140,255,.18); }
|
|
69
|
+
.thread .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
|
70
|
+
.thread .meta { font-size: 11px; color: rgba(255,255,255,.4); flex: 0 0 auto; }
|
|
71
|
+
.thread .dot { width: 7px; height: 7px; border-radius: 50%; background: #2997ff; flex: 0 0 auto; box-shadow: 0 0 0 1px rgba(41,151,255,.3); }
|
|
72
|
+
.skill-row { display: flex; align-items: center; gap: 8px; height: 26px; font-size: 12.5px; color: rgba(255,255,255,.78); padding: 0 8px; border-radius: 8px; cursor: default; }
|
|
73
|
+
.skill-row:hover { background: rgba(255,255,255,.05); }
|
|
74
|
+
.skill-row .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
75
|
+
|
|
76
|
+
/* ---- content ---- */
|
|
77
|
+
.content { position: absolute; left: 285px; top: 0; right: 0; bottom: 0; color: var(--text); background: linear-gradient(180deg, rgba(37,37,39,.98), rgba(35,35,37,.98)); display: flex; flex-direction: column; }
|
|
78
|
+
.topbar { height: 40px; flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 14px 0 16px; }
|
|
79
|
+
.title { font-size: 13px; color: rgba(255,255,255,.86); display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
80
|
+
.title .crumb { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
81
|
+
.toolbar { display: flex; gap: 8px; }
|
|
82
|
+
.toolbar button { width: 34px; height: 28px; border-radius: 10px; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.1); display: grid; place-items: center; padding: 0; cursor: pointer; color: rgba(255,255,255,.82); }
|
|
83
|
+
.toolbar button:hover { background: rgba(255,255,255,.08); }
|
|
84
|
+
.toolbar button:disabled { opacity: .35; cursor: not-allowed; }
|
|
85
|
+
|
|
86
|
+
.messages { flex: 1; overflow-y: auto; position: relative; padding: 8px 0 24px; }
|
|
87
|
+
.messages::-webkit-scrollbar { width: 10px; } .messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 5px; }
|
|
88
|
+
|
|
89
|
+
.empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; transform: translateY(-3%); }
|
|
90
|
+
.logo { width: 52px; height: 52px; color: rgba(255,255,255,.95); }
|
|
91
|
+
.build-text { font-size: 34px; line-height: 1; font-weight: 500; letter-spacing: -0.03em; color: rgba(255,255,255,.96); }
|
|
92
|
+
|
|
93
|
+
.pill { position: relative; height: 32px; padding: 0 13px 0 11px; border-radius: 14px; background: #3a3a3d; border: 1px solid rgba(255,255,255,.05); display: inline-flex; align-items: center; gap: 7px; color: rgba(255,255,255,.82); font-size: 13.5px; cursor: pointer; }
|
|
94
|
+
.pill:hover { background: #424246; }
|
|
95
|
+
.pill .caret { opacity: .6; }
|
|
96
|
+
.pill-menu { position: absolute; bottom: 40px; left: 0; min-width: 220px; max-height: 320px; overflow-y: auto; background: #2a2a2d; border: 1px solid rgba(255,255,255,.1); border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,.4); padding: 6px; z-index: 50; display: none; }
|
|
97
|
+
.pill-menu.open { display: block; }
|
|
98
|
+
.pill-menu .item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; font-size: 13px; color: rgba(255,255,255,.85); cursor: pointer; }
|
|
99
|
+
.pill-menu .item:hover { background: rgba(255,255,255,.08); }
|
|
100
|
+
.pill-menu .item.active { color: #8aa6ff; }
|
|
101
|
+
.pill-menu .item .check { width: 14px; opacity: 0; } .pill-menu .item.active .check { opacity: 1; }
|
|
102
|
+
.pill-menu .hint { padding: 8px 10px; font-size: 11.5px; color: rgba(255,255,255,.45); }
|
|
103
|
+
|
|
104
|
+
/* chat bubbles */
|
|
105
|
+
.stream { max-width: 760px; margin: 0 auto; padding: 0 28px; display: flex; flex-direction: column; gap: 18px; }
|
|
106
|
+
.msg { display: flex; flex-direction: column; gap: 8px; }
|
|
107
|
+
.msg.user { align-items: flex-end; }
|
|
108
|
+
.bubble { max-width: 100%; border-radius: 16px; padding: 12px 15px; font-size: 14.5px; line-height: 1.55; word-wrap: break-word; overflow-wrap: anywhere; }
|
|
109
|
+
.msg.user .bubble { background: #2f5bff; color: #fff; border-bottom-right-radius: 5px; max-width: 78%; }
|
|
110
|
+
.msg.assistant .bubble { background: #2b2b2e; border: 1px solid rgba(255,255,255,.06); border-bottom-left-radius: 5px; }
|
|
111
|
+
.msg.assistant .bubble.streaming::after { content: "▋"; color: #8aa6ff; animation: blink 1s steps(2) infinite; margin-left: 1px; }
|
|
112
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
113
|
+
.role-tag { font-size: 11px; color: rgba(255,255,255,.4); padding: 0 4px; }
|
|
114
|
+
.msg.assistant .role-tag { align-self: flex-start; }
|
|
115
|
+
|
|
116
|
+
/* turn folding + tool chips + file refs (inside assistant bubbles) */
|
|
117
|
+
.turn { margin: 6px 0; }
|
|
118
|
+
.turn-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: rgba(255,255,255,.6); background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 4px 9px; cursor: pointer; user-select: none; }
|
|
119
|
+
.turn-chip:hover { background: rgba(255,255,255,.1); }
|
|
120
|
+
.turn-chip .tri { font-size: 9px; transition: transform .15s; } .turn-chip.open .tri { transform: rotate(90deg); }
|
|
121
|
+
.turn-body { margin-top: 6px; }
|
|
122
|
+
.turn.collapsed .turn-body { display: none; }
|
|
123
|
+
.tool-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; color: #c9d4ff; background: rgba(107,140,255,.14); border: 1px solid rgba(107,140,255,.22); border-radius: 7px; padding: 3px 8px; margin: 4px 0; }
|
|
124
|
+
.file-ref { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; color: #8fd0a8; background: rgba(143,208,168,.1); border: 1px solid rgba(143,208,168,.2); border-radius: 6px; padding: 2px 7px; cursor: pointer; }
|
|
125
|
+
.file-ref:hover { background: rgba(143,208,168,.18); }
|
|
126
|
+
|
|
127
|
+
/* markdown */
|
|
128
|
+
.md p { margin: 0 0 8px; } .md p:last-child { margin-bottom: 0; }
|
|
129
|
+
.md h1,.md h2,.md h3 { margin: 12px 0 6px; line-height: 1.3; font-weight: 600; }
|
|
130
|
+
.md h1 { font-size: 18px; } .md h2 { font-size: 16px; } .md h3 { font-size: 14.5px; }
|
|
131
|
+
.md ul,.md ol { margin: 6px 0 8px; padding-left: 22px; } .md li { margin: 2px 0; }
|
|
132
|
+
.md a { color: #8aa6ff; text-decoration: none; } .md a:hover { text-decoration: underline; }
|
|
133
|
+
.md strong { font-weight: 600; color: #fff; }
|
|
134
|
+
.md code { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 13px; background: rgba(255,255,255,.08); padding: 1px 5px; border-radius: 5px; }
|
|
135
|
+
.md pre { background: #161617; border: 1px solid rgba(255,255,255,.07); border-radius: 10px; padding: 12px 13px; overflow-x: auto; margin: 8px 0; }
|
|
136
|
+
.md pre code { background: none; padding: 0; font-size: 13px; line-height: 1.5; color: #e7e7ea; }
|
|
137
|
+
.md blockquote { border-left: 3px solid rgba(107,140,255,.5); margin: 8px 0; padding: 2px 12px; color: rgba(255,255,255,.7); }
|
|
138
|
+
.md table { border-collapse: collapse; margin: 8px 0; font-size: 13px; }
|
|
139
|
+
.md th,.md td { border: 1px solid rgba(255,255,255,.12); padding: 5px 9px; }
|
|
140
|
+
.md hr { border: none; border-top: 1px solid rgba(255,255,255,.1); margin: 10px 0; }
|
|
141
|
+
|
|
142
|
+
.err-bubble { background: rgba(239,68,68,.12) !important; border: 1px solid rgba(239,68,68,.3) !important; color: #ffb4b4; }
|
|
143
|
+
|
|
144
|
+
/* ---- composer ---- */
|
|
145
|
+
.composer { flex: 0 0 auto; padding: 0 0 22px; }
|
|
146
|
+
.composer-inner { max-width: 637px; margin: 0 auto; }
|
|
147
|
+
.input-shell { position: relative; background: #353537; border: 1px solid rgba(255,255,255,.09); border-radius: 26px; box-shadow: inset 0 1px 0 rgba(255,255,255,.02); padding: 14px 56px 14px 16px; min-height: 56px; }
|
|
148
|
+
.input-shell:focus-within { border-color: rgba(107,140,255,.5); }
|
|
149
|
+
#input { width: 100%; background: none; border: none; outline: none; resize: none; color: var(--text); font-family: inherit; font-size: 14.5px; line-height: 1.5; max-height: 200px; min-height: 22px; }
|
|
150
|
+
#input::placeholder { color: rgba(255,255,255,.42); }
|
|
151
|
+
.plus { position: absolute; left: 14px; bottom: 12px; font-size: 26px; line-height: 1; color: rgba(255,255,255,.7); cursor: pointer; font-weight: 300; user-select: none; }
|
|
152
|
+
.plus:hover { color: #fff; }
|
|
153
|
+
.send { position: absolute; right: 10px; bottom: 10px; width: 36px; height: 36px; border-radius: 50%; background: #f4f3f2; display: grid; place-items: center; color: #1c1c1d; cursor: pointer; }
|
|
154
|
+
.send:hover { background: #fff; }
|
|
155
|
+
.send.stop { background: #ff5f57; color: #fff; }
|
|
156
|
+
.send:disabled { opacity: .4; cursor: not-allowed; }
|
|
157
|
+
.attach-row { display: none; margin-top: 8px; gap: 6px; }
|
|
158
|
+
.attach-row.show { display: flex; }
|
|
159
|
+
.attach-row input { flex: 1; background: #2b2b2d; border: 1px solid rgba(255,255,255,.1); border-radius: 8px; color: var(--text); font-family: ui-monospace, monospace; font-size: 12.5px; padding: 6px 9px; outline: none; }
|
|
160
|
+
|
|
161
|
+
.footer-row { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; padding: 0 14px 0 16px; font-size: 13px; color: rgba(255,255,255,.55); }
|
|
162
|
+
.modes { display: flex; gap: 16px; align-items: center; }
|
|
163
|
+
.mode { cursor: pointer; padding: 2px 4px; border-radius: 6px; }
|
|
164
|
+
.mode:hover { color: rgba(255,255,255,.8); }
|
|
165
|
+
.mode.active { color: rgba(255,255,255,.92); font-weight: 500; }
|
|
166
|
+
.branch { display: flex; align-items: center; gap: 6px; }
|
|
167
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #28c840; box-shadow: 0 0 0 1px rgba(0,0,0,.15); }
|
|
168
|
+
.status-dot.running { background: #febc2e; animation: pulse 1.1s ease-in-out infinite; }
|
|
169
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
|
|
170
|
+
|
|
171
|
+
.toast { position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%); background: #2a2a2d; border: 1px solid rgba(255,255,255,.12); color: rgba(255,255,255,.9); padding: 10px 16px; border-radius: 10px; font-size: 13px; box-shadow: 0 10px 30px rgba(0,0,0,.4); opacity: 0; transition: opacity .2s, transform .2s; z-index: 100; pointer-events: none; }
|
|
172
|
+
.toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
|
|
173
|
+
|
|
174
|
+
.setup-banner { display: none; margin: 0 28px 12px; max-width: 760px; margin-left: auto; margin-right: auto; background: rgba(239,168,71,.12); border: 1px solid rgba(239,168,71,.35); color: #ffd9a0; border-radius: 12px; padding: 10px 14px; font-size: 13px; }
|
|
175
|
+
.setup-banner.show { display: block; }
|
|
176
|
+
|
|
177
|
+
/* ---- video gallery + modal ---- */
|
|
178
|
+
.video-row { display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 8px; border-radius: 8px; cursor: pointer; font-size: 12.5px; color: rgba(255,255,255,.82); }
|
|
179
|
+
.video-row:hover { background: rgba(255,255,255,.06); }
|
|
180
|
+
.video-row .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
181
|
+
.video-row .meta { font-size: 10.5px; color: rgba(255,255,255,.4); flex: 0 0 auto; }
|
|
182
|
+
.video-mode-tag { display: inline-block; font-size: 10px; background: rgba(107,140,255,.18); border: 1px solid rgba(107,140,255,.25); color: #a0b8ff; border-radius: 4px; padding: 1px 5px; margin-left: 6px; vertical-align: middle; }
|
|
183
|
+
.video-modal { position: fixed; inset: 0; background: rgba(0,0,0,.82); display: none; align-items: center; justify-content: center; z-index: 200; }
|
|
184
|
+
.video-modal.open { display: flex; }
|
|
185
|
+
.video-modal .box { max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; gap: 12px; align-items: center; }
|
|
186
|
+
.video-modal video { max-width: 88vw; max-height: 78vh; border-radius: 12px; background: #000; }
|
|
187
|
+
.video-modal .cap { color: rgba(255,255,255,.8); font-size: 13px; font-family: ui-monospace, monospace; max-width: 80vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
188
|
+
.video-modal .close { position: fixed; top: 20px; right: 24px; width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,.12); border: none; color: #fff; font-size: 20px; cursor: pointer; display: grid; place-items: center; }
|
|
189
|
+
.video-modal .close:hover { background: rgba(255,255,255,.22); }
|
|
190
|
+
.video-modal .dl { font-size: 12.5px; color: #8aa6ff; cursor: pointer; text-decoration: underline; }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<main class="desktop">
|
|
195
|
+
<section class="window">
|
|
196
|
+
<aside class="sidebar">
|
|
197
|
+
<div class="sidebar-inner">
|
|
198
|
+
<div class="traffic"><span class="red"></span><span class="yellow"></span><span class="green"></span></div>
|
|
199
|
+
|
|
200
|
+
<div class="nav-item" id="nav-new" title="New conversation">
|
|
201
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z"/></svg>
|
|
202
|
+
<span>New thread</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="nav-item" id="nav-suggest" title="Ask GA to suggest tasks">
|
|
205
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/></svg>
|
|
206
|
+
<span>Suggest tasks</span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="nav-item" id="nav-skills" title="Skill tree (memory/SOPs)">
|
|
209
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7l9 5 9-5-9-5Z"/><path d="M3 17l9 5 9-5"/><path d="M3 12l9 5 9-5"/></svg>
|
|
210
|
+
<span>Skills</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="nav-item" id="nav-video" title="Make a video with HyperFrames">
|
|
213
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="14" height="14" rx="2"/><path d="m22 8-6 4 6 4V8Z"/></svg>
|
|
214
|
+
<span>Make video</span>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="section-title">Threads</div>
|
|
218
|
+
<div class="scroll" id="sessions-scroll">
|
|
219
|
+
<div id="session-list" style="color:rgba(255,255,255,.35);font-size:12.5px;padding:6px 8px">Loading…</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="section-title" id="skills-title">Skills · memory</div>
|
|
223
|
+
<div class="scroll" id="skills-scroll" style="max-height:180px">
|
|
224
|
+
<div id="skill-list" style="color:rgba(255,255,255,.35);font-size:12.5px;padding:6px 8px">—</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="section-title" id="videos-title">Videos · HyperFrames</div>
|
|
228
|
+
<div class="scroll" id="videos-scroll" style="max-height:160px">
|
|
229
|
+
<div id="video-list" style="color:rgba(255,255,255,.35);font-size:12.5px;padding:6px 8px">—</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</aside>
|
|
233
|
+
|
|
234
|
+
<section class="content">
|
|
235
|
+
<div class="topbar">
|
|
236
|
+
<div class="title">
|
|
237
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.6)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v1H3V7Z"/><path d="M3 8h18l-1.3 9.1A2 2 0 0 1 17.7 19H6.3a2 2 0 0 1-2-1.9L3 8Z"/></svg>
|
|
238
|
+
<span class="crumb" id="title">New thread</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="toolbar">
|
|
241
|
+
<button id="btn-stop" title="Stop current task" disabled>
|
|
242
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
|
243
|
+
</button>
|
|
244
|
+
<button id="btn-new" title="New conversation (clear context)">
|
|
245
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="setup-banner" id="setup-banner">⚠️ No LLM configured. Create <code>mykey.py</code> in the GenericAgent root with your API key, then restart <code>webui_codex.py</code>.</div>
|
|
251
|
+
|
|
252
|
+
<div class="messages" id="messages">
|
|
253
|
+
<div class="empty" id="empty">
|
|
254
|
+
<svg class="logo" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
255
|
+
<path d="M32 7c6.7 0 12 4.7 12 10.5 0 .9-.1 1.8-.4 2.7 5 1.2 8.9 5.5 8.9 10.9 0 4.8-3 8.9-7.2 10.6.4 1.1.7 2.3.7 3.5 0 5.8-5.3 10.5-12 10.5-4 0-7.4-1.8-9.5-4.5-1 .4-2.2.5-3.3.5-6.7 0-12-4.7-12-10.5 0-3.7 2.1-6.9 5.3-8.8-.8-1.8-1.3-3.8-1.3-5.8C13.2 18 20 12 28 12c1.5 0 2.9.2 4.2.6C29 10.2 30.3 7 32 7Z"/>
|
|
256
|
+
<path d="M22 24c-2 1.6-3 4-3 6.7 0 4.6 3.5 8.3 8 8.3 2 0 3.9-.7 5.3-1.9"/>
|
|
257
|
+
<path d="M25 34c0 3.8 2.9 6.8 6.4 6.8 2.1 0 3.8-.8 5.4-2.4"/>
|
|
258
|
+
<path d="M28 43.3c2.4-1.2 4.5-3.2 6-5.8 1.6-2.8 2.5-6 2.5-9.6 0-2.9-.6-5.8-1.8-8.1"/>
|
|
259
|
+
</svg>
|
|
260
|
+
<div class="build-text">Let’s build</div>
|
|
261
|
+
<div class="pill" id="llm-pill">
|
|
262
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M9 9h6v6H9z"/></svg>
|
|
263
|
+
<span id="llm-pill-label">—</span>
|
|
264
|
+
<svg class="caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
|
265
|
+
<div class="pill-menu" id="llm-menu"></div>
|
|
266
|
+
</div>
|
|
267
|
+
<div style="font-size:13px;color:rgba(255,255,255,.42);max-width:480px;text-align:center;line-height:1.6;margin-top:4px">
|
|
268
|
+
Ask anything — drive your computer, browse, code, automate. Each task the agent solves crystallizes into a Skill.<br>
|
|
269
|
+
<span style="opacity:.7">Slash cmds: <code style="font-family:ui-monospace,monospace;background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px">/btw</code> <code style="font-family:ui-monospace,monospace;background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px">/review</code> <code style="font-family:ui-monospace,monospace;background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px">/resume</code></span>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="stream" id="stream"></div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="composer">
|
|
276
|
+
<div class="composer-inner">
|
|
277
|
+
<div class="setup-banner" id="setup-banner-bottom" style="margin:0 0 8px"></div>
|
|
278
|
+
<div class="input-shell">
|
|
279
|
+
<textarea id="input" rows="1" placeholder="Ask anything · ⏎ send · ⇧⏎ newline"></textarea>
|
|
280
|
+
<div class="plus" id="btn-attach" title="Attach a file path for the agent to read">+</div>
|
|
281
|
+
<div class="send" id="btn-send" title="Send">
|
|
282
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19V5"/><path d="m6 11 6-6 6 6"/></svg>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="attach-row" id="attach-row">
|
|
286
|
+
<input id="attach-input" placeholder="/absolute/path/to/file — inserted into the prompt for the agent to read" />
|
|
287
|
+
</div>
|
|
288
|
+
<div class="footer-row">
|
|
289
|
+
<div class="modes">
|
|
290
|
+
<span class="mode active" data-mode="Local">Local</span>
|
|
291
|
+
<span class="mode" data-mode="Worktree">Worktree</span>
|
|
292
|
+
<span class="mode" data-mode="Cloud">Cloud</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="branch">
|
|
295
|
+
<span class="status-dot" id="status-dot"></span>
|
|
296
|
+
<span id="branch-label">idle</span>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</section>
|
|
302
|
+
</section>
|
|
303
|
+
</main>
|
|
304
|
+
<div class="video-modal" id="video-modal">
|
|
305
|
+
<button class="close" id="video-close">×</button>
|
|
306
|
+
<div class="box">
|
|
307
|
+
<video id="video-player" controls autoplay></video>
|
|
308
|
+
<div class="cap" id="video-cap"></div>
|
|
309
|
+
<a class="dl" id="video-dl" download>⬇ download</a>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="toast" id="toast"></div>
|
|
313
|
+
|
|
314
|
+
<script>
|
|
315
|
+
"use strict";
|
|
316
|
+
const $ = (id) => document.getElementById(id);
|
|
317
|
+
const state = { running: false, configured: true, llms: [], llmNo: 0, llmName: "—", sessionPath: null };
|
|
318
|
+
|
|
319
|
+
// ---------- tiny utils ----------
|
|
320
|
+
function toast(msg) {
|
|
321
|
+
const t = $("toast"); t.textContent = msg; t.classList.add("show");
|
|
322
|
+
clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove("show"), 2600);
|
|
323
|
+
}
|
|
324
|
+
async function api(path, opts) {
|
|
325
|
+
const res = await fetch(path, opts || {});
|
|
326
|
+
const ct = res.headers.get("content-type") || "";
|
|
327
|
+
if (ct.includes("application/json")) return { _status: res.status, ...(await res.json()) };
|
|
328
|
+
const txt = await res.text(); return { _status: res.status, _text: txt };
|
|
329
|
+
}
|
|
330
|
+
function esc(s) { return (s || "").replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">","\"":""","'":"'"}[c])); }
|
|
331
|
+
function relTime(ms) {
|
|
332
|
+
const d = Math.floor(Date.now()/1000 - ms);
|
|
333
|
+
if (d < 60) return d + "s"; if (d < 3600) return Math.floor(d/60) + "m";
|
|
334
|
+
if (d < 86400) return Math.floor(d/3600) + "h"; return Math.floor(d/86400) + "d";
|
|
335
|
+
}
|
|
336
|
+
function autoGrow(el) { el.style.height = "auto"; el.style.height = Math.min(el.scrollHeight, 200) + "px"; }
|
|
337
|
+
|
|
338
|
+
// ---------- minimal markdown ----------
|
|
339
|
+
function md(src) {
|
|
340
|
+
src = (src || "").replace(/\r/g, "");
|
|
341
|
+
// pull out fenced code blocks first
|
|
342
|
+
const blocks = []; let i = 0;
|
|
343
|
+
src = src.replace(/```([\w+-]*)\n([\s\S]*?)```/g, (m, lang, code) => {
|
|
344
|
+
blocks.push('<pre><code>' + esc(code.replace(/\n$/, "")) + '</code></pre>'); return `\x00BLOCK${i++}\x00`;
|
|
345
|
+
});
|
|
346
|
+
// inline code
|
|
347
|
+
src = esc(src).replace(/`([^`\n]+)`/g, (_, c) => '<code>' + c + '</code>');
|
|
348
|
+
// tables (simple pipe tables)
|
|
349
|
+
src = src.replace(/((?:^\|.*\|\n)+)/gm, (tbl) => {
|
|
350
|
+
const rows = tbl.trim().split("\n").map(r => r.replace(/^\||\|$/g, "").split("|"));
|
|
351
|
+
if (rows.length < 2) return tbl;
|
|
352
|
+
const sep = rows[1].map(c => /^[\s:-]+$/.test(c)); if (!sep.every(Boolean)) return tbl;
|
|
353
|
+
const head = rows[0], body = rows.slice(2);
|
|
354
|
+
let h = "<table><thead><tr>" + head.map(c => "<th>" + c.trim() + "</th>").join("") + "</tr></thead><tbody>";
|
|
355
|
+
for (const r of body) h += "<tr>" + r.map(c => "<td>" + c.trim() + "</td>").join("") + "</tr>";
|
|
356
|
+
return h + "</tbody></table>";
|
|
357
|
+
});
|
|
358
|
+
const lines = src.split("\n"); let out = "", list = null;
|
|
359
|
+
const flush = () => { if (list) { out += `</${list}>`; list = null; } };
|
|
360
|
+
for (let raw of lines) {
|
|
361
|
+
const line = raw;
|
|
362
|
+
if (/^\x00BLOCK\d+\x00$/.test(line.trim())) { flush(); out += blocks[+line.trim().match(/\d+/)[0]]; continue; }
|
|
363
|
+
if (/^\s*$/.test(line)) { flush(); out += "\n"; continue; }
|
|
364
|
+
let m;
|
|
365
|
+
if (m = line.match(/^(#{1,3})\s+(.*)/)) { flush(); out += `<h${m[1].length}>${m[2]}</h${m[1].length}>`; continue; }
|
|
366
|
+
if (m = line.match(/^>\s?(.*)/)) { flush(); out += `<blockquote>${m[1]}</blockquote>`; continue; }
|
|
367
|
+
if (m = line.match(/^[-*]\s+(.*)/)) { if (list !== "ul") { flush(); out += "<ul>"; list = "ul"; } out += `<li>${m[1]}</li>`; continue; }
|
|
368
|
+
if (m = line.match(/^\d+\.\s+(.*)/)) { if (list !== "ol") { flush(); out += "<ol>"; list = "ol"; } out += `<li>${m[1]}</li>`; continue; }
|
|
369
|
+
if (/^(?:---|\*\*\*)\s*$/.test(line)) { flush(); out += "<hr>"; continue; }
|
|
370
|
+
out += line + " ";
|
|
371
|
+
}
|
|
372
|
+
flush();
|
|
373
|
+
// paragraphs
|
|
374
|
+
out = out.replace(/\n{2,}/g, "</p><p>").replace(/^\s+|\s+$/g, "");
|
|
375
|
+
out = "<p>" + out + "</p>";
|
|
376
|
+
// links
|
|
377
|
+
out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
|
378
|
+
out = out.replace(/(^|[\s(])((?:https?:\/\/)[^\s<)]+)(?=[\s<)|]|$)/g, '$1<a href="$2" target="_blank" rel="noreferrer">$2</a>');
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------- turn folding + tool/file decoration ----------
|
|
383
|
+
const TURN_RE = /\*\*LLM Running \(Turn \d+\)\s*\.\.\.\*\*/g;
|
|
384
|
+
const TOOL_RE = /🛠️\s*Tool:\s*`([^`]+)`[^\n]*\n?/g;
|
|
385
|
+
|
|
386
|
+
function renderAssistant(container, rawText, streaming) {
|
|
387
|
+
container.innerHTML = "";
|
|
388
|
+
let text = (rawText || "").replace(/\r/g, "");
|
|
389
|
+
// strip thinking blocks (collapsed away for cleanliness)
|
|
390
|
+
text = text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "");
|
|
391
|
+
// split into turns
|
|
392
|
+
const parts = text.split(TURN_RE);
|
|
393
|
+
// parts[0] = first turn body; subsequent entries alternate (the marker is consumed)
|
|
394
|
+
const turns = [];
|
|
395
|
+
if (parts.length === 1) {
|
|
396
|
+
turns.push({ n: 0, body: parts[0] });
|
|
397
|
+
} else {
|
|
398
|
+
turns.push({ n: 0, body: parts[0] });
|
|
399
|
+
for (let k = 1; k < parts.length; k++) turns.push({ n: k, body: parts[k] });
|
|
400
|
+
}
|
|
401
|
+
turns.forEach((t, idx) => {
|
|
402
|
+
const isLive = streaming && idx === turns.length - 1;
|
|
403
|
+
const collapsed = idx > 0 && !isLive;
|
|
404
|
+
const wrap = document.createElement("div");
|
|
405
|
+
wrap.className = "turn" + (collapsed ? " collapsed" : "");
|
|
406
|
+
if (idx > 0) {
|
|
407
|
+
const chip = document.createElement("div");
|
|
408
|
+
chip.className = "turn-chip" + (collapsed ? "" : " open");
|
|
409
|
+
chip.innerHTML = '<span class="tri">▶</span> Turn ' + idx;
|
|
410
|
+
chip.onclick = () => { const open = chip.classList.toggle("open"); wrap.classList.toggle("collapsed", !open); };
|
|
411
|
+
wrap.appendChild(chip);
|
|
412
|
+
}
|
|
413
|
+
const body = document.createElement("div");
|
|
414
|
+
body.className = "turn-body";
|
|
415
|
+
let bhtml = decorate(t.body);
|
|
416
|
+
body.innerHTML = '<div class="md">' + bhtml + '</div>';
|
|
417
|
+
wrap.appendChild(body);
|
|
418
|
+
container.appendChild(wrap);
|
|
419
|
+
});
|
|
420
|
+
if (!turns.some(t => (t.body || "").trim())) container.innerHTML = '<div class="md"><p style="opacity:.5">…</p></div>';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function decorate(text) {
|
|
424
|
+
// file references [FILE:path] -> chips
|
|
425
|
+
let out = text.replace(/\[FILE:([^\]]+)\]/g, (_, p) => {
|
|
426
|
+
const base = p.split(/[\\/]/).pop();
|
|
427
|
+
return `<span class="file-ref" data-path="${esc(p)}" title="${esc(p)}">📄 ${esc(base)}</span>`;
|
|
428
|
+
});
|
|
429
|
+
// tool call header lines -> chips
|
|
430
|
+
out = out.replace(TOOL_RE, (_, name) => `<div><span class="tool-chip">🛠 ${esc(name)}</span></div>`);
|
|
431
|
+
// summary highlight
|
|
432
|
+
out = out.replace(/<summary>([\s\S]*?)<\/summary>/g, (_, s) => `<blockquote><strong>summary</strong><br>${s}</blockquote>`);
|
|
433
|
+
// leftover raw tags
|
|
434
|
+
out = out.replace(/<\/?(?:thinking|summary|tool_use|file_content)>/g, "");
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ---------- bubbles ----------
|
|
439
|
+
function addBubble(role, content, opts) {
|
|
440
|
+
opts = opts || {};
|
|
441
|
+
const empty = $("empty"); if (empty) empty.style.display = "none";
|
|
442
|
+
const msg = document.createElement("div"); msg.className = "msg " + role;
|
|
443
|
+
const tag = document.createElement("div"); tag.className = "role-tag"; tag.textContent = role === "user" ? "you" : "agent"; if (role === "user") tag.style.alignSelf = "flex-end";
|
|
444
|
+
const bub = document.createElement("div"); bub.className = "bubble" + (opts.error ? " err-bubble" : "");
|
|
445
|
+
if (role === "user") { bub.textContent = content; }
|
|
446
|
+
else { bub.classList.add("content-host"); renderAssistant(bub, content, !!opts.streaming); if (opts.streaming) bub.classList.add("streaming"); }
|
|
447
|
+
msg.appendChild(tag); msg.appendChild(bub);
|
|
448
|
+
$("stream").appendChild(msg);
|
|
449
|
+
$("messages").scrollTop = $("messages").scrollHeight;
|
|
450
|
+
return bub;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------- status / sidebar ----------
|
|
454
|
+
function setRunning(on) {
|
|
455
|
+
state.running = on;
|
|
456
|
+
$("status-dot").classList.toggle("running", on);
|
|
457
|
+
$("branch-label").textContent = on ? "running" : "idle";
|
|
458
|
+
$("btn-stop").disabled = !on;
|
|
459
|
+
const send = $("btn-send"); send.classList.toggle("stop", on);
|
|
460
|
+
send.title = on ? "Stop" : "Send";
|
|
461
|
+
send.innerHTML = on
|
|
462
|
+
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>'
|
|
463
|
+
: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19V5"/><path d="m6 11 6-6 6 6"/></svg>';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function loadStatus() {
|
|
467
|
+
const s = await api("/api/status");
|
|
468
|
+
state.configured = s.configured !== false;
|
|
469
|
+
$("setup-banner").classList.toggle("show", !state.configured);
|
|
470
|
+
$("setup-banner-bottom").classList.toggle("show", !state.configured);
|
|
471
|
+
if (!state.configured) {
|
|
472
|
+
$("setup-banner-bottom").innerHTML = '⚠️ No LLM configured — set <code>mykey.py</code> and restart.';
|
|
473
|
+
}
|
|
474
|
+
state.llms = s.llms || []; state.llmNo = s.llm_no; state.llmName = s.llm_name || "—";
|
|
475
|
+
$("llm-pill-label").textContent = state.llmName;
|
|
476
|
+
$("title").textContent = s.log ? "session · " + s.log.replace("model_responses_", "") : "New thread";
|
|
477
|
+
setRunning(!!s.running);
|
|
478
|
+
renderLlmMenu();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderLlmMenu() {
|
|
482
|
+
const menu = $("llm-menu"); menu.innerHTML = "";
|
|
483
|
+
if (!state.llms.length) { menu.innerHTML = '<div class="hint">no LLM configured</div>'; return; }
|
|
484
|
+
state.llms.forEach(l => {
|
|
485
|
+
const it = document.createElement("div");
|
|
486
|
+
it.className = "item" + (l.idx === state.llmNo ? " active" : "");
|
|
487
|
+
it.innerHTML = '<span class="check">✓</span><span>' + esc(l.name) + '</span>';
|
|
488
|
+
it.onclick = async (e) => { e.stopPropagation(); menu.classList.remove("open"); await switchLlm(l.idx); };
|
|
489
|
+
menu.appendChild(it);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function switchLlm(idx) {
|
|
494
|
+
const r = await api("/api/llm", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ idx }) });
|
|
495
|
+
if (r.error) { toast("LLM switch failed: " + r.error); return; }
|
|
496
|
+
state.llmNo = r.llm_no; state.llmName = r.llm_name; state.llms = r.llms || state.llms;
|
|
497
|
+
$("llm-pill-label").textContent = state.llmName; renderLlmMenu(); toast("LLM → " + state.llmName);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function loadSessions() {
|
|
501
|
+
const r = await api("/api/sessions"); const list = $("session-list");
|
|
502
|
+
if (r.error || !r.items || !r.items.length) { list.innerHTML = '<div style="color:rgba(255,255,255,.3);font-size:12.5px;padding:6px 8px">No sessions yet</div>'; return; }
|
|
503
|
+
list.innerHTML = "";
|
|
504
|
+
r.items.slice(0, 30).forEach((s, i) => {
|
|
505
|
+
const row = document.createElement("div"); row.className = "thread" + (s.current ? " active" : "");
|
|
506
|
+
row.innerHTML = (s.current ? '<span class="dot"></span>' : "") + '<span class="label">' + esc(s.preview || s.name) + '</span><span class="meta">' + s.rounds + "r · " + relTime(s.mtime) + '</span>';
|
|
507
|
+
row.title = s.path;
|
|
508
|
+
row.onclick = () => continueSession(i + 1, s);
|
|
509
|
+
list.appendChild(row);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function loadSkills() {
|
|
514
|
+
const r = await api("/api/skills"); const list = $("skill-list");
|
|
515
|
+
if (r.error || !r.items || !r.items.length) { list.innerHTML = '<div style="color:rgba(255,255,255,.3);font-size:12.5px;padding:6px 8px">No skills yet</div>'; return; }
|
|
516
|
+
list.innerHTML = "";
|
|
517
|
+
r.items.slice(0, 40).forEach(s => {
|
|
518
|
+
const row = document.createElement("div"); row.className = "skill-row";
|
|
519
|
+
row.innerHTML = '<span>' + (s.is_dir ? "📁" : "📄") + '</span><span class="label">' + esc(s.name) + '</span>';
|
|
520
|
+
row.title = s.path; list.appendChild(row);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ---------- video gallery ----------
|
|
525
|
+
async function loadVideos() {
|
|
526
|
+
const r = await api("/api/videos"); const list = $("video-list");
|
|
527
|
+
if (r.error || !r.items || !r.items.length) { list.innerHTML = '<div style="color:rgba(255,255,255,.3);font-size:12.5px;padding:6px 8px">No renders yet</div>'; return; }
|
|
528
|
+
list.innerHTML = "";
|
|
529
|
+
r.items.slice(0, 20).forEach(v => {
|
|
530
|
+
const row = document.createElement("div"); row.className = "video-row";
|
|
531
|
+
row.innerHTML = '<span>🎬</span><span class="label">' + esc(v.name) + '</span><span class="meta">' + relTime(v.mtime*1000) + '</span>';
|
|
532
|
+
row.title = v.path; row.onclick = () => openVideo(v); list.appendChild(row);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function openVideo(v) {
|
|
536
|
+
$("video-player").src = "/api/video/" + encodeURIComponent(v.path);
|
|
537
|
+
$("video-cap").textContent = v.name + " · " + (v.size/1024/1024).toFixed(1) + " MB";
|
|
538
|
+
$("video-dl").href = "/api/video/" + encodeURIComponent(v.path);
|
|
539
|
+
$("video-dl").download = v.name;
|
|
540
|
+
$("video-modal").classList.add("open");
|
|
541
|
+
}
|
|
542
|
+
$("video-close").onclick = () => { $("video-modal").classList.remove("open"); $("video-player").pause(); $("video-player").src=""; };
|
|
543
|
+
$("video-modal").onclick = (e) => { if (e.target === $("video-modal")) $("video-close").click(); };
|
|
544
|
+
|
|
545
|
+
// ---------- make video ----------
|
|
546
|
+
function makeVideo() {
|
|
547
|
+
const ta = $("input");
|
|
548
|
+
ta.value = ""; autoGrow(ta);
|
|
549
|
+
ta.placeholder = "Describe the video you want — e.g. \"a 15s product launch video for my SaaS, 16:9, with captions\"";
|
|
550
|
+
ta.focus();
|
|
551
|
+
state.videoMode = true;
|
|
552
|
+
toast("HyperFrames video mode — type your video idea and press Enter");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------- actions ----------
|
|
556
|
+
async function send() {
|
|
557
|
+
if (state.running) { await stop(); return; }
|
|
558
|
+
const ta = $("input"); const text = ta.value.trim(); if (!text) return;
|
|
559
|
+
if (!state.configured) { toast("No LLM configured — set mykey.py"); return; }
|
|
560
|
+
const attach = $("attach-input").value.trim();
|
|
561
|
+
const full = attach ? text + "\n\n(参考文件:" + attach + ")" : text;
|
|
562
|
+
ta.value = ""; autoGrow(ta); $("attach-row").classList.remove("show"); $("attach-input").value = "";
|
|
563
|
+
addBubble("user", full);
|
|
564
|
+
const bub = addBubble("assistant", "", { streaming: true });
|
|
565
|
+
setRunning(true);
|
|
566
|
+
|
|
567
|
+
const isBtw = /^\/btw(\s|$)/.test(text);
|
|
568
|
+
if (isBtw) {
|
|
569
|
+
const q = text.replace(/^\/btw\s*/, "");
|
|
570
|
+
const r = await api("/api/btw", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ q }) });
|
|
571
|
+
bub.classList.remove("streaming");
|
|
572
|
+
if (r.error) { bub.classList.add("err-bubble"); renderAssistant(bub, r.error, false); }
|
|
573
|
+
else renderAssistant(bub, r.text || "*(empty)*", false);
|
|
574
|
+
setRunning(false); loadSessions(); return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const isVideo = state.videoMode;
|
|
578
|
+
state.videoMode = false;
|
|
579
|
+
ta.placeholder = "Ask anything · ⏎ send · ⇧⏎ newline";
|
|
580
|
+
|
|
581
|
+
// /video <desc> slash command → set video mode for this turn
|
|
582
|
+
const videoCmd = text.match(/^\/video(\s+([\s\S]+))?$/);
|
|
583
|
+
const useVideo = isVideo || !!videoCmd;
|
|
584
|
+
const sendText = videoCmd ? (videoCmd[2] || "").trim() : full;
|
|
585
|
+
if (videoCmd && !sendText) { toast("Usage: /video <description>"); setRunning(false); return; }
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const res = await fetch("/api/chat", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ text: sendText, video: useVideo }) });
|
|
589
|
+
if (res.status === 409) { const j = await res.json(); bub.classList.remove("streaming"); bub.classList.add("err-bubble"); renderAssistant(bub, j.error || "agent busy", false); setRunning(false); return; }
|
|
590
|
+
if (!res.ok || !res.body) { const t = await res.text(); bub.classList.remove("streaming"); bub.classList.add("err-bubble"); renderAssistant(bub, t || ("HTTP " + res.status), false); setRunning(false); return; }
|
|
591
|
+
const reader = res.body.getReader(); const dec = new TextDecoder(); let buf = ""; let live = "";
|
|
592
|
+
while (true) {
|
|
593
|
+
const { value, done } = await reader.read(); if (done) break;
|
|
594
|
+
buf += dec.decode(value, { stream: true });
|
|
595
|
+
let nl;
|
|
596
|
+
while ((nl = buf.indexOf("\n\n")) >= 0) {
|
|
597
|
+
const chunk = buf.slice(0, nl); buf = buf.slice(nl + 2);
|
|
598
|
+
const line = chunk.replace(/^data:\s*/, "").trim(); if (!line) continue;
|
|
599
|
+
let ev; try { ev = JSON.parse(line); } catch { continue; }
|
|
600
|
+
if (ev.type === "delta") { live += ev.text; renderAssistant(bub, live, true); $("messages").scrollTop = $("messages").scrollHeight; }
|
|
601
|
+
else if (ev.type === "done") {
|
|
602
|
+
live = ev.text || live; bub.classList.remove("streaming"); renderAssistant(bub, live, false);
|
|
603
|
+
if (ev.files && ev.files.length) {
|
|
604
|
+
const fbox = document.createElement("div"); fbox.style.marginTop = "8px";
|
|
605
|
+
fbox.innerHTML = '<span style="font-size:11px;color:rgba(255,255,255,.4)">produced: </span>' + ev.files.map(p => { const b = p.split(/[\\/]/).pop(); return `<span class="file-ref" data-path="${esc(p)}" title="${esc(p)}">📄 ${esc(b)}</span>`; }).join(" ");
|
|
606
|
+
bub.appendChild(fbox);
|
|
607
|
+
}
|
|
608
|
+
} else if (ev.type === "error") { bub.classList.remove("streaming"); bub.classList.add("err-bubble"); renderAssistant(bub, ev.message || "error", false); }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} catch (e) { bub.classList.remove("streaming"); bub.classList.add("err-bubble"); renderAssistant(bub, "network: " + e.message, false); }
|
|
612
|
+
setRunning(false); loadStatus(); loadSessions(); loadVideos();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function stop() { await api("/api/stop", { method: "POST" }); toast("stop signal sent"); setRunning(false); }
|
|
616
|
+
async function newThread() {
|
|
617
|
+
const r = await api("/api/new", { method: "POST" });
|
|
618
|
+
$("stream").innerHTML = ""; $("empty").style.display = ""; $("title").textContent = "New thread";
|
|
619
|
+
toast(r.message || "new conversation"); loadStatus(); loadSessions();
|
|
620
|
+
}
|
|
621
|
+
async function continueSession(idx, sess) {
|
|
622
|
+
const r = await api("/api/continue", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ idx }) });
|
|
623
|
+
if (r.error) { toast("restore failed: " + r.error); return; }
|
|
624
|
+
$("stream").innerHTML = ""; $("empty").style.display = "none";
|
|
625
|
+
// replay bubbles
|
|
626
|
+
if (r.path) {
|
|
627
|
+
const m = await api("/api/messages?path=" + encodeURIComponent(r.path));
|
|
628
|
+
(m.items || []).forEach(it => addBubble(it.role, it.content || ""));
|
|
629
|
+
} else { addBubble("assistant", r.message || "restored", {}); }
|
|
630
|
+
$("title").textContent = "session · " + (sess.name || "").replace("model_responses_", "");
|
|
631
|
+
toast(r.message || "session restored"); loadStatus(); loadSessions();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ---------- wiring ----------
|
|
635
|
+
$("btn-send").onclick = send;
|
|
636
|
+
$("btn-stop").onclick = stop;
|
|
637
|
+
$("btn-new").onclick = newThread;
|
|
638
|
+
$("nav-new").onclick = newThread;
|
|
639
|
+
$("nav-suggest").onclick = () => { $("input").value = "Following the planning section of your autonomous SOP, analyze my situation and generate a batch of TODOs that will interest me."; autoGrow($("input")); $("input").focus(); };
|
|
640
|
+
$("nav-skills").onclick = () => { $("skills-scroll").scrollTop = 0; loadSkills(); };
|
|
641
|
+
$("nav-video").onclick = makeVideo;
|
|
642
|
+
|
|
643
|
+
const pill = $("llm-pill");
|
|
644
|
+
pill.onclick = (e) => { if (e.target.closest(".pill-menu")) return; $("llm-menu").classList.toggle("open"); };
|
|
645
|
+
document.addEventListener("click", (e) => { if (!e.target.closest("#llm-pill")) $("llm-menu").classList.remove("open"); });
|
|
646
|
+
|
|
647
|
+
$("btn-attach").onclick = () => { const r = $("attach-row"); r.classList.toggle("show"); if (r.classList.contains("show")) $("attach-input").focus(); };
|
|
648
|
+
$("attach-input").addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); $("input").focus(); } });
|
|
649
|
+
|
|
650
|
+
document.querySelectorAll(".mode").forEach(m => m.onclick = () => {
|
|
651
|
+
if (m.dataset.mode !== "Local") { toast(m.dataset.mode + " mode is visual only — Local drives the agent"); return; }
|
|
652
|
+
document.querySelectorAll(".mode").forEach(x => x.classList.remove("active")); m.classList.add("active");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const ta = $("input");
|
|
656
|
+
ta.addEventListener("input", () => autoGrow(ta));
|
|
657
|
+
ta.addEventListener("keydown", e => {
|
|
658
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
document.addEventListener("click", e => {
|
|
662
|
+
const fr = e.target.closest(".file-ref");
|
|
663
|
+
if (fr) { navigator.clipboard && navigator.clipboard.writeText(fr.dataset.path); toast("copied path: " + fr.dataset.path); }
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// boot
|
|
667
|
+
(async () => {
|
|
668
|
+
await loadStatus(); loadSessions(); loadSkills(); loadVideos();
|
|
669
|
+
setInterval(loadStatus, 4000); setInterval(loadSessions, 15000); setInterval(loadVideos, 20000);
|
|
670
|
+
})();
|
|
671
|
+
</script>
|
|
672
|
+
</body>
|
|
673
|
+
</html>
|