Files changed (8) hide show
  1. algorithm.py +203 -0
  2. app.py +49 -1526
  3. code_builder.py +46 -0
  4. code_highlighting.py +103 -0
  5. constants.py +76 -0
  6. javascript_handler.py +880 -0
  7. ui_handlers.py +59 -0
  8. ui_styles.py +570 -0
algorithm.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ROSA-QKV 算法实现及步骤追踪
3
+ """
4
+
5
+ import json
6
+ import time
7
+ from typing import List, Dict, Any, Tuple
8
+
9
+
10
+ def find_line_number(lines: list[str], needle: str, fallback: int = 1) -> int:
11
+ """在代码行列表中查找包含特定字符串的行号"""
12
+ for index, line in enumerate(lines, start=1):
13
+ if needle in line:
14
+ return index
15
+ return fallback
16
+
17
+
18
+ def find_line_after(
19
+ lines: list[str], start_line: int, needle: str, fallback: int
20
+ ) -> int:
21
+ """从指定行开始查找包含特定字符串的行号"""
22
+ start_index = min(max(start_line, 0), len(lines))
23
+ for index in range(start_index, len(lines)):
24
+ if needle in lines[index]:
25
+ return index + 1
26
+ return fallback
27
+
28
+
29
+ def initialize_line_numbers(code: str) -> Dict[str, int]:
30
+ """初始化代码中各关键行的行号"""
31
+ lines = code.splitlines()
32
+
33
+ return {
34
+ "LINE_FOR_I": find_line_number(lines, "for i in range(n):"),
35
+ "LINE_FOR_W": find_line_number(lines, "for w in range(i+1,0,-1):"),
36
+ "LINE_T_ASSIGN": find_line_number(lines, "t=qqq[i+1-w:i+1]"),
37
+ "LINE_FOR_J": find_line_number(lines, "for j in range(i-w,-1,-1):"),
38
+ "LINE_TRY": find_line_number(lines, "if kkk[j:j+w]==t:"),
39
+ "LINE_ASSIGN": find_line_number(lines, "out[i]=vvv[j+w]"),
40
+ "LINE_BREAK_INNER": find_line_after(
41
+ lines,
42
+ find_line_number(lines, "out[i]=vvv[j+w]"),
43
+ "break",
44
+ find_line_number(lines, "out[i]=vvv[j+w]"),
45
+ ),
46
+ "LINE_IF_OUT": find_line_number(lines, "if out[i]!=-1:"),
47
+ "LINE_BREAK_OUTER": find_line_after(
48
+ lines,
49
+ find_line_number(lines, "if out[i]!=-1:"),
50
+ "break",
51
+ find_line_number(lines, "if out[i]!=-1:"),
52
+ ),
53
+ "LINE_RETURN": find_line_number(lines, "return out"),
54
+ }
55
+
56
+
57
+ def rosa_steps(
58
+ q: list[int], k: list[int], v: list[int], line_numbers: Dict[str, int]
59
+ ) -> Tuple[List[Dict], List[int]]:
60
+ """
61
+ 执行 ROSA-QKV 算法并记录每个步骤
62
+ 返回: (步骤列表, 输出列表)
63
+ """
64
+ n = len(q)
65
+ out = [-1] * n
66
+ steps: list[dict] = []
67
+
68
+ for i in range(n):
69
+ steps.append({"phase": "loop_i", "i": i, "line": line_numbers["LINE_FOR_I"]})
70
+ matched = False
71
+ match_j = -1
72
+ match_w = -1
73
+
74
+ for w in range(i + 1, 0, -1):
75
+ steps.append(
76
+ {"phase": "loop_w", "i": i, "w": w, "line": line_numbers["LINE_FOR_W"]}
77
+ )
78
+ t = q[i + 1 - w : i + 1]
79
+ t_str = "".join(str(x) for x in t)
80
+ steps.append(
81
+ {
82
+ "phase": "assign_t",
83
+ "i": i,
84
+ "w": w,
85
+ "t": t_str,
86
+ "line": line_numbers["LINE_T_ASSIGN"],
87
+ }
88
+ )
89
+
90
+ for j in range(i - w, -1, -1):
91
+ steps.append(
92
+ {
93
+ "phase": "loop_j",
94
+ "i": i,
95
+ "w": w,
96
+ "j": j,
97
+ "line": line_numbers["LINE_FOR_J"],
98
+ }
99
+ )
100
+ is_match = k[j : j + w] == t
101
+ steps.append(
102
+ {
103
+ "phase": "try",
104
+ "i": i,
105
+ "w": w,
106
+ "j": j,
107
+ "t": t_str,
108
+ "matched": is_match,
109
+ "line": line_numbers["LINE_TRY"],
110
+ }
111
+ )
112
+
113
+ if is_match:
114
+ value = v[j + w]
115
+ out[i] = value
116
+ match_j = j
117
+ match_w = w
118
+ steps.append(
119
+ {
120
+ "phase": "assign",
121
+ "i": i,
122
+ "w": w,
123
+ "j": j,
124
+ "t": t_str,
125
+ "v_index": j + w,
126
+ "value": value,
127
+ "line": line_numbers["LINE_ASSIGN"],
128
+ }
129
+ )
130
+ steps.append(
131
+ {
132
+ "phase": "break_inner",
133
+ "i": i,
134
+ "w": w,
135
+ "j": j,
136
+ "line": line_numbers["LINE_BREAK_INNER"],
137
+ }
138
+ )
139
+ matched = True
140
+ break
141
+
142
+ if matched:
143
+ steps.append(
144
+ {
145
+ "phase": "check",
146
+ "i": i,
147
+ "w": match_w,
148
+ "j": match_j,
149
+ "matched": True,
150
+ "line": line_numbers["LINE_IF_OUT"],
151
+ }
152
+ )
153
+ if i == n - 1:
154
+ steps.append(
155
+ {
156
+ "phase": "break_outer",
157
+ "i": i,
158
+ "w": match_w,
159
+ "j": match_j,
160
+ "line": line_numbers["LINE_BREAK_OUTER"],
161
+ }
162
+ )
163
+ break
164
+
165
+ if not matched:
166
+ out[i] = -1
167
+
168
+ steps.append(
169
+ {
170
+ "phase": "output",
171
+ "i": i,
172
+ "value": out[i],
173
+ "matched": matched,
174
+ }
175
+ )
176
+
177
+ if i == n - 1:
178
+ steps.append({"phase": "return", "line": line_numbers["LINE_RETURN"]})
179
+
180
+ return steps, out
181
+
182
+
183
+ def format_output(out: list[int]) -> str:
184
+ """格式化输出结果"""
185
+ if any(value < 0 or value > 1 for value in out):
186
+ return " ".join(str(value) for value in out)
187
+ return "".join(str(value) for value in out)
188
+
189
+
190
+ def build_payload(
191
+ q: list[int], k: list[int], v: list[int], line_numbers: Dict[str, int]
192
+ ) -> Tuple[str, str]:
193
+ """构建 JSON 负载和格式化输出"""
194
+ steps, out = rosa_steps(q, k, v, line_numbers)
195
+ payload = {
196
+ "q": q,
197
+ "k": k,
198
+ "v": v,
199
+ "steps": steps,
200
+ "out": out,
201
+ "run_id": time.time_ns(),
202
+ }
203
+ return json.dumps(payload, ensure_ascii=False), format_output(out)
app.py CHANGED
@@ -1,1534 +1,31 @@
1
- import html
2
- import json
3
- import random
4
- import re
5
- import time
6
-
7
- import gradio as gr
8
-
9
- MAX_DEMO_LEN = 20
10
- GRADIO_MAJOR = int((gr.__version__ or "0").split(".", maxsplit=1)[0])
11
-
12
- ROSA_CODE = """
13
- def rosa_qkv_naive(qqq, kkk, vvv):
14
- n=len(qqq); out=[-1]*n
15
- for i in range(n):
16
- for w in range(i+1,0,-1):
17
- t=qqq[i+1-w:i+1]
18
- for j in range(i-w,-1,-1):
19
- if kkk[j:j+w]==t:
20
- out[i]=vvv[j+w]
21
- break
22
- if out[i]!=-1:
23
- break
24
- return out
25
- """.strip(
26
- "\n"
27
- )
28
-
29
- ROSA_QUICK_CODE = """
30
- def rosa_qkv_ref_minus1(qqq, kkk, vvv): # note: input will never contain "-1"
31
- n=len(qqq); y=[-1]*n; s=2*n+1; t=[None]*s; f=[-1]*s; m=[0]*s; r=[-1]*s; t[0]={}; g=0; u=1; w=h=0; assert n==len(kkk)==len(vvv)
32
- for i,(q,k) in enumerate(zip(qqq,kkk)):
33
- p,x=w,h
34
- while p!=-1 and q not in t[p]: x=m[p] if x>m[p] else x; p=f[p]
35
- p,x=(t[p][q],x+1) if p!=-1 else (0,0); v=p
36
- while f[v]!=-1 and m[f[v]]>=x: v=f[v]
37
- while v!=-1 and (m[v]<=0 or r[v]<0): v=f[v]
38
- y[i]=vvv[r[v]+1] if v!=-1 else -1; w,h=p,x; j=u; u+=1; t[j]={}; m[j]=m[g]+1; p=g
39
- while p!=-1 and k not in t[p]: t[p][k]=j; p=f[p]
40
- if p==-1: f[j]=0
41
- else:
42
- d=t[p][k]
43
- if m[p]+1==m[d]: f[j]=d
44
- else:
45
- b=u; u+=1; t[b]=t[d].copy(); m[b]=m[p]+1; f[b]=f[d]; r[b]=r[d]; f[d]=f[j]=b
46
- while p!=-1 and t[p][k]==d: t[p][k]=b; p=f[p]
47
- v=g=j
48
- while v!=-1 and r[v]<i: r[v]=i; v=f[v]
49
- return y
50
- """.strip(
51
- "\n"
52
- )
53
-
54
-
55
- KEYWORDS = {
56
- "def",
57
- "for",
58
- "in",
59
- "if",
60
- "else",
61
- "while",
62
- "break",
63
- "return",
64
- "assert",
65
- "None",
66
- "True",
67
- "False",
68
- }
69
- BUILTINS = {"len", "range", "zip", "enumerate"}
70
- KEYWORD_RE = re.compile(r"\b(" + "|".join(sorted(KEYWORDS)) + r")\b")
71
- BUILTIN_RE = re.compile(r"\b(" + "|".join(sorted(BUILTINS)) + r")\b")
72
- NUMBER_RE = re.compile(r"(?<![\w.])(-?\d+)(?![\w.])")
73
-
74
-
75
- def split_comment(line: str) -> tuple[str, str]:
76
- in_single = False
77
- in_double = False
78
- escaped = False
79
- for index, ch in enumerate(line):
80
- if escaped:
81
- escaped = False
82
- continue
83
- if ch == "\\":
84
- escaped = True
85
- continue
86
- if ch == "'" and not in_double:
87
- in_single = not in_single
88
- continue
89
- if ch == '"' and not in_single:
90
- in_double = not in_double
91
- continue
92
- if ch == "#" and not in_single and not in_double:
93
- return line[:index], line[index:]
94
- return line, ""
95
-
96
-
97
- def tokenize_strings(code: str) -> list[tuple[str, str]]:
98
- segments: list[tuple[str, str]] = []
99
- index = 0
100
- while index < len(code):
101
- ch = code[index]
102
- if ch in ("'", '"'):
103
- quote = ch
104
- start = index
105
- index += 1
106
- escaped = False
107
- while index < len(code):
108
- if escaped:
109
- escaped = False
110
- index += 1
111
- continue
112
- if code[index] == "\\":
113
- escaped = True
114
- index += 1
115
- continue
116
- if code[index] == quote:
117
- index += 1
118
- break
119
- index += 1
120
- segments.append(("string", code[start:index]))
121
- continue
122
- start = index
123
- while index < len(code) and code[index] not in ("'", '"'):
124
- index += 1
125
- segments.append(("text", code[start:index]))
126
- return segments
127
-
128
-
129
- def highlight_text_segment(text: str) -> str:
130
- escaped = html.escape(text)
131
- escaped = KEYWORD_RE.sub(r'<span class="tok-keyword">\1</span>', escaped)
132
- escaped = BUILTIN_RE.sub(r'<span class="tok-builtin">\1</span>', escaped)
133
- escaped = NUMBER_RE.sub(r'<span class="tok-number">\1</span>', escaped)
134
- return escaped
135
-
136
-
137
- def highlight_python_line(line: str) -> str:
138
- code, comment = split_comment(line)
139
- segments = tokenize_strings(code)
140
- rendered: list[str] = []
141
- for kind, text in segments:
142
- if kind == "string":
143
- rendered.append('<span class="tok-string">{}</span>'.format(html.escape(text)))
144
- else:
145
- rendered.append(highlight_text_segment(text))
146
- if comment:
147
- rendered.append('<span class="tok-comment">{}</span>'.format(html.escape(comment)))
148
- return "".join(rendered)
149
-
150
-
151
- def build_code_html(code: str) -> str:
152
- lines = code.splitlines()
153
- rendered = ['<div id="rosa-code" class="rosa-code">']
154
- marker_t = "__TOK_T__"
155
- marker_k = "__TOK_K__"
156
- marker_v = "__TOK_V__"
157
- for index, line in enumerate(lines, start=1):
158
- line_with_markers = line
159
- if index == LINE_TRY:
160
- line_with_markers = line_with_markers.replace("kkk[j:j+w]", marker_k, 1)
161
- line_with_markers = line_with_markers.replace("t", marker_t, 1)
162
- if index == LINE_ASSIGN:
163
- line_with_markers = line_with_markers.replace("vvv[j+w]", marker_v, 1)
164
- highlighted = highlight_python_line(line_with_markers)
165
- highlighted = highlighted.replace(marker_t, '<span class="code-token" data-token="t">t</span>')
166
- highlighted = highlighted.replace(marker_k, '<span class="code-token" data-token="k">kkk[j:j+w]</span>')
167
- highlighted = highlighted.replace(marker_v, '<span class="code-token" data-token="v">vvv[j+w]</span>')
168
- rendered.append(
169
- '<div class="code-line" data-line="{line}">'
170
- '<span class="line-no">{line}</span>'
171
- '<span class="line-text">{text}</span>'
172
- "</div>".format(line=index, text=highlighted)
173
- )
174
- rendered.append("</div>")
175
- return "\n".join(rendered)
176
-
177
-
178
- def build_plain_code_html(code: str, block_id: str) -> str:
179
- lines = code.splitlines()
180
- rendered = [f'<div id="{block_id}" class="rosa-code">']
181
- for index, line in enumerate(lines, start=1):
182
- highlighted = highlight_python_line(line)
183
- rendered.append(
184
- '<div class="code-line">'
185
- '<span class="line-no">{line}</span>'
186
- '<span class="line-text">{text}</span>'
187
- "</div>".format(line=index, text=highlighted)
188
- )
189
- rendered.append("</div>")
190
- return "\n".join(rendered)
191
-
192
-
193
- def find_line_number(lines: list[str], needle: str, fallback: int = 1) -> int:
194
- for index, line in enumerate(lines, start=1):
195
- if needle in line:
196
- return index
197
- return fallback
198
-
199
-
200
- ROSA_LINES = ROSA_CODE.splitlines()
201
-
202
-
203
- def find_line_after(lines: list[str], start_line: int, needle: str, fallback: int) -> int:
204
- start_index = min(max(start_line, 0), len(lines))
205
- for index in range(start_index, len(lines)):
206
- if needle in lines[index]:
207
- return index + 1
208
- return fallback
209
-
210
-
211
- LINE_FOR_I = find_line_number(ROSA_LINES, "for i in range(n):")
212
- LINE_FOR_W = find_line_number(ROSA_LINES, "for w in range(i+1,0,-1):")
213
- LINE_T_ASSIGN = find_line_number(ROSA_LINES, "t=qqq[i+1-w:i+1]")
214
- LINE_FOR_J = find_line_number(ROSA_LINES, "for j in range(i-w,-1,-1):")
215
- LINE_TRY = find_line_number(ROSA_LINES, "if kkk[j:j+w]==t:")
216
- LINE_ASSIGN = find_line_number(ROSA_LINES, "out[i]=vvv[j+w]")
217
- LINE_BREAK_INNER = find_line_after(ROSA_LINES, LINE_ASSIGN, "break", LINE_ASSIGN)
218
- LINE_IF_OUT = find_line_number(ROSA_LINES, "if out[i]!=-1:")
219
- LINE_BREAK_OUTER = find_line_after(ROSA_LINES, LINE_IF_OUT, "break", LINE_IF_OUT)
220
- LINE_RETURN = find_line_number(ROSA_LINES, "return out")
221
- CODE_HTML = build_code_html(ROSA_CODE)
222
- QUICK_CODE_HTML = build_plain_code_html(ROSA_QUICK_CODE, "rosa-code-quick")
223
-
224
-
225
- def parse_bits(text: str, name: str) -> list[int]:
226
- cleaned = re.sub(r"[,\s]+", "", (text or "").strip())
227
- if not cleaned:
228
- raise gr.Error(f"{name} cannot be empty")
229
- if re.search(r"[^01]", cleaned):
230
- raise gr.Error(f"{name} must contain only 0/1 (spaces or commas allowed)")
231
- return [int(c) for c in cleaned]
232
-
233
-
234
- def rosa_steps(q: list[int], k: list[int], v: list[int]) -> tuple[list[dict], list[int]]:
235
- n = len(q)
236
- out = [-1] * n
237
- steps: list[dict] = []
238
- for i in range(n):
239
- steps.append({"phase": "loop_i", "i": i, "line": LINE_FOR_I})
240
- matched = False
241
- match_j = -1
242
- match_w = -1
243
- for w in range(i + 1, 0, -1):
244
- steps.append({"phase": "loop_w", "i": i, "w": w, "line": LINE_FOR_W})
245
- t = q[i + 1 - w : i + 1]
246
- t_str = "".join(str(x) for x in t)
247
- steps.append({"phase": "assign_t", "i": i, "w": w, "t": t_str, "line": LINE_T_ASSIGN})
248
- for j in range(i - w, -1, -1):
249
- steps.append({"phase": "loop_j", "i": i, "w": w, "j": j, "line": LINE_FOR_J})
250
- is_match = k[j : j + w] == t
251
- steps.append(
252
- {
253
- "phase": "try",
254
- "i": i,
255
- "w": w,
256
- "j": j,
257
- "t": t_str,
258
- "matched": is_match,
259
- "line": LINE_TRY,
260
- }
261
- )
262
- if is_match:
263
- value = v[j + w]
264
- out[i] = value
265
- match_j = j
266
- match_w = w
267
- steps.append(
268
- {
269
- "phase": "assign",
270
- "i": i,
271
- "w": w,
272
- "j": j,
273
- "t": t_str,
274
- "v_index": j + w,
275
- "value": value,
276
- "line": LINE_ASSIGN,
277
- }
278
- )
279
- steps.append(
280
- {
281
- "phase": "break_inner",
282
- "i": i,
283
- "w": w,
284
- "j": j,
285
- "line": LINE_BREAK_INNER,
286
- }
287
- )
288
- matched = True
289
- break
290
- if matched:
291
- steps.append(
292
- {
293
- "phase": "check",
294
- "i": i,
295
- "w": match_w,
296
- "j": match_j,
297
- "matched": True,
298
- "line": LINE_IF_OUT,
299
- }
300
- )
301
- if i == n - 1:
302
- steps.append(
303
- {
304
- "phase": "break_outer",
305
- "i": i,
306
- "w": match_w,
307
- "j": match_j,
308
- "line": LINE_BREAK_OUTER,
309
- }
310
- )
311
- break
312
- if not matched:
313
- out[i] = -1
314
- steps.append(
315
- {
316
- "phase": "output",
317
- "i": i,
318
- "value": out[i],
319
- "matched": matched,
320
- }
321
- )
322
- if i == n - 1:
323
- steps.append({"phase": "return", "line": LINE_RETURN})
324
- return steps, out
325
-
326
-
327
- def format_out(out: list[int]) -> str:
328
- if any(value < 0 or value > 1 for value in out):
329
- return " ".join(str(value) for value in out)
330
- return "".join(str(value) for value in out)
331
-
332
-
333
- def build_payload(q: list[int], k: list[int], v: list[int]) -> tuple[str, str]:
334
- steps, out = rosa_steps(q, k, v)
335
- payload = {
336
- "q": q,
337
- "k": k,
338
- "v": v,
339
- "steps": steps,
340
- "out": out,
341
- "run_id": time.time_ns(),
342
- }
343
- return json.dumps(payload, ensure_ascii=False), format_out(out)
344
-
345
-
346
- def on_demo(q_text: str, k_text: str, v_text: str) -> tuple[str, str]:
347
- q = parse_bits(q_text, "q")
348
- k = parse_bits(k_text, "k")
349
- v = parse_bits(v_text, "v")
350
- if not (len(q) == len(k) == len(v)):
351
- raise gr.Error("q, k, v must have the same length")
352
- if len(q) > MAX_DEMO_LEN:
353
- raise gr.Error(f"For smooth playback, length should be <= {MAX_DEMO_LEN}")
354
- return build_payload(q, k, v)
355
-
356
-
357
- def on_random(length: int) -> tuple[str, str, str]:
358
- length = max(1, int(length))
359
- q = [random.randint(0, 1) for _ in range(length)]
360
- k = [random.randint(0, 1) for _ in range(length)]
361
- v = [random.randint(0, 1) for _ in range(length)]
362
- return "".join(str(x) for x in q), "".join(str(x) for x in k), "".join(str(x) for x in v)
363
-
364
-
365
- CSS = """
366
- .page-header {
367
- text-align: center;
368
- margin-bottom: 18px;
369
- color: #0f172a;
370
- }
371
- .page-title {
372
- font-size: 30px;
373
- font-weight: 700;
374
- letter-spacing: 0.4px;
375
- margin-bottom: 6px;
376
- }
377
- .page-subtitle {
378
- font-size: 14px;
379
- color: #475569;
380
- }
381
- .rosa-shell {
382
- display: flex;
383
- gap: 24px;
384
- align-items: flex-start;
385
- justify-content: center;
386
- flex-wrap: wrap;
387
- position: relative;
388
- }
389
- .rosa-pane {
390
- flex: 3 1 520px;
391
- min-width: 320px;
392
- }
393
- .rosa-code-pane {
394
- flex: 2 1 320px;
395
- min-width: 300px;
396
- }
397
- .quick-code-details {
398
- margin-top: 8px;
399
- }
400
- .quick-code-details > summary {
401
- cursor: pointer;
402
- user-select: none;
403
- padding: 8px 10px;
404
- border: 1px dashed #cbd5e1;
405
- border-radius: 12px;
406
- background: #f8fafc;
407
- color: #0f172a;
408
- font-weight: 600;
409
- }
410
- .quick-code-details[open] > summary {
411
- margin-bottom: 10px;
412
- }
413
- .quick-code-details > summary::-webkit-details-marker {
414
- display: none;
415
- }
416
- .quick-code-details .rosa-code {
417
- max-height: 420px;
418
- }
419
- #rosa-vis {
420
- --rosa-blue: #3b82f6;
421
- --rosa-sky: #38bdf8;
422
- --rosa-violet: #a855f7;
423
- --rosa-amber: #f59e0b;
424
- --rosa-cyan: #06b6d4;
425
- --rosa-green: #22c55e;
426
- --rosa-green-soft: rgba(34, 197, 94, 0.16);
427
- --rosa-red: #ef4444;
428
- --rosa-red-soft: rgba(239, 68, 68, 0.14);
429
- --rosa-blue-soft: rgba(59, 130, 246, 0.08);
430
- --rosa-violet-soft: rgba(168, 85, 247, 0.08);
431
- }
432
- #rosa-vis .rosa-card {
433
- background: #ffffff;
434
- border-radius: 18px;
435
- padding: 24px;
436
- color: #0f172a;
437
- box-shadow: none;
438
- border: 1px solid #e2e8f0;
439
- text-align: center;
440
- position: relative;
441
- }
442
- #rosa-vis .rosa-rows {
443
- display: flex;
444
- flex-direction: column;
445
- gap: 20px;
446
- align-items: center;
447
- }
448
- #rosa-vis .rosa-row {
449
- display: flex;
450
- align-items: center;
451
- gap: 14px;
452
- flex-wrap: wrap;
453
- justify-content: center;
454
- width: 100%;
455
- }
456
- #rosa-vis .rosa-row.k-row {
457
- margin-bottom: 0;
458
- }
459
- #rosa-vis .row-label {
460
- min-width: 36px;
461
- font-size: 12px;
462
- color: #475569;
463
- text-transform: uppercase;
464
- letter-spacing: 0.2px;
465
- text-align: center;
466
- }
467
- #rosa-vis .row-cells {
468
- display: flex;
469
- flex-wrap: wrap;
470
- gap: 6px;
471
- justify-content: center;
472
- max-width: 100%;
473
- min-width: 0;
474
- }
475
- #rosa-vis .cell {
476
- width: 30px;
477
- height: 30px;
478
- border-radius: 8px;
479
- background: #f1f5f9;
480
- display: flex;
481
- align-items: center;
482
- justify-content: center;
483
- font-weight: 600;
484
- transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
485
- color: #0f172a;
486
- box-shadow: none;
487
- }
488
- #rosa-vis .cell.active {
489
- background: var(--rosa-blue);
490
- color: #f8fafc;
491
- box-shadow: none;
492
- }
493
- #rosa-vis .cell.suffix {
494
- background: var(--rosa-sky);
495
- color: #f8fafc;
496
- }
497
- #rosa-vis .cell.k-window {
498
- background: var(--rosa-violet);
499
- color: #f8fafc;
500
- }
501
- #rosa-vis .cell.v-pick {
502
- background: var(--rosa-amber);
503
- color: #1f2937;
504
- }
505
- #rosa-vis .cell.out,
506
- #rosa-vis .cell.out-fixed {
507
- background: var(--rosa-amber);
508
- color: #1f2937;
509
- }
510
- #rosa-vis .cell.filled {
511
- box-shadow: none;
512
- }
513
- #rosa-vis .rosa-overlay {
514
- position: absolute;
515
- inset: 0;
516
- pointer-events: none;
517
- z-index: 5;
518
- }
519
- #rosa-vis .overlay-svg {
520
- position: absolute;
521
- inset: 0;
522
- width: 100%;
523
- height: 100%;
524
- pointer-events: none;
525
- }
526
- #rosa-vis .overlay-box-layer {
527
- position: absolute;
528
- inset: 0;
529
- pointer-events: none;
530
- }
531
- #rosa-vis .overlay-box {
532
- position: absolute;
533
- border: 2px solid rgba(59, 130, 246, 0.7);
534
- border-radius: 10px;
535
- box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
536
- box-sizing: border-box;
537
- background: transparent;
538
- transition: border-color 0.18s ease, background 0.18s ease;
539
- }
540
- #rosa-vis .overlay-hover-layer {
541
- position: absolute;
542
- inset: 0;
543
- pointer-events: auto;
544
- z-index: 6;
545
- }
546
- #rosa-vis .overlay-hover-box {
547
- position: absolute;
548
- background: transparent;
549
- pointer-events: auto;
550
- cursor: pointer;
551
- }
552
- #rosa-vis .overlay-box[data-label="t"] {
553
- --overlay-label: "t";
554
- }
555
- #rosa-vis .overlay-box[data-label="kkk[j:j+w]"] {
556
- --overlay-label: "kkk[j:j+w]";
557
- }
558
- #rosa-vis .overlay-box.t-box {
559
- border-color: var(--rosa-blue);
560
- background: var(--rosa-blue-soft);
561
- }
562
- #rosa-vis .overlay-box.k-box {
563
- border-color: var(--rosa-violet);
564
- background: var(--rosa-violet-soft);
565
- }
566
- #rosa-vis .overlay-box.try-match {
567
- border-color: var(--rosa-green);
568
- background: var(--rosa-green-soft);
569
- }
570
- #rosa-vis .overlay-box.try-miss {
571
- border-color: var(--rosa-red);
572
- background: var(--rosa-red-soft);
573
- }
574
- #rosa-vis .overlay-ray {
575
- position: absolute;
576
- inset: 0;
577
- width: 100%;
578
- height: 100%;
579
- pointer-events: none;
580
- }
581
- #rosa-vis .overlay-ray-line {
582
- stroke: var(--rosa-cyan);
583
- stroke-width: 2;
584
- fill: none;
585
- stroke-linecap: round;
586
- }
587
- #rosa-vis .overlay-label {
588
- position: absolute;
589
- top: -12px;
590
- right: 0;
591
- font-size: 11px;
592
- padding: 0;
593
- background: transparent;
594
- color: #0f172a;
595
- letter-spacing: 0.2px;
596
- white-space: nowrap;
597
- display: none;
598
- }
599
- #rosa-vis .overlay-label.t-label {
600
- color: #1d4ed8;
601
- }
602
- #rosa-vis .overlay-label.k-label {
603
- color: #6d28d9;
604
- }
605
- .v-float {
606
- position: fixed;
607
- z-index: 9999;
608
- border-radius: 8px;
609
- background: #f59e0b;
610
- color: #1f2937;
611
- display: flex;
612
- align-items: center;
613
- justify-content: center;
614
- font-weight: 600;
615
- box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
616
- pointer-events: none;
617
- transition: transform 0.35s ease, opacity 0.35s ease;
618
- opacity: 1;
619
- }
620
- #rosa-vis .rosa-legend {
621
- display: flex;
622
- flex-wrap: wrap;
623
- gap: 12px;
624
- margin-bottom: 12px;
625
- font-size: 12px;
626
- color: #475569;
627
- justify-content: center;
628
- }
629
- #rosa-vis .legend-item {
630
- display: inline-flex;
631
- align-items: center;
632
- gap: 6px;
633
- }
634
- #rosa-vis .legend-dot {
635
- width: 12px;
636
- height: 12px;
637
- border-radius: 4px;
638
- }
639
- #rosa-vis .legend-suffix {
640
- background: var(--rosa-sky);
641
- }
642
- #rosa-vis .legend-window {
643
- background: var(--rosa-violet);
644
- }
645
- #rosa-vis .legend-match {
646
- background: var(--rosa-green);
647
- }
648
- #rosa-vis .legend-v {
649
- background: var(--rosa-amber);
650
- }
651
- #rosa-vis .legend-out {
652
- background: var(--rosa-amber);
653
- }
654
- #rosa-vis .index-row {
655
- padding-bottom: 8px;
656
- border-bottom: 1px dashed #e2e8f0;
657
- }
658
- #rosa-vis .index-cells {
659
- gap: 6px;
660
- }
661
- #rosa-vis .index-cell {
662
- width: 30px;
663
- text-align: center;
664
- font-size: 11px;
665
- color: #64748b;
666
- font-variant-numeric: tabular-nums;
667
- }
668
- .rosa-code {
669
- background: #ffffff;
670
- border: 1px solid #e2e8f0;
671
- border-radius: 12px;
672
- padding: 12px;
673
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
674
- font-size: 12px;
675
- color: #0f172a;
676
- max-height: 520px;
677
- overflow: auto;
678
- }
679
- .rosa-code .code-line {
680
- display: flex;
681
- gap: 10px;
682
- padding: 2px 6px;
683
- border-radius: 6px;
684
- }
685
- .rosa-code .line-no {
686
- flex: 0 0 36px;
687
- width: 36px;
688
- text-align: left;
689
- color: #94a3b8;
690
- user-select: none;
691
- font-variant-numeric: tabular-nums;
692
- }
693
- .rosa-code .line-text {
694
- white-space: pre;
695
- flex: 1;
696
- }
697
- .rosa-code .code-line.active {
698
- background: #dbeafe;
699
- color: #1d4ed8;
700
- }
701
- .rosa-code .code-line.active .line-no {
702
- color: #1d4ed8;
703
- }
704
- .rosa-code .tok-keyword {
705
- color: #7c3aed;
706
- font-weight: 600;
707
- }
708
- .rosa-code .tok-builtin {
709
- color: #0ea5e9;
710
- }
711
- .rosa-code .tok-number {
712
- color: #f97316;
713
- }
714
- .rosa-code .tok-string {
715
- color: #10b981;
716
- }
717
- .rosa-code .tok-comment {
718
- color: #64748b;
719
- font-style: italic;
720
- }
721
- .rosa-code .code-token {
722
- border-bottom: 1px dashed #94a3b8;
723
- padding: 0 1px;
724
- }
725
- .rosa-code .code-token.active {
726
- background: #fef3c7;
727
- border-radius: 4px;
728
- border-bottom-color: #f59e0b;
729
- }
730
  """
731
-
732
- JS_FUNC = """
733
- () => {
734
- if (window.__rosaDemoInit) return;
735
- window.__rosaDemoInit = true;
736
-
737
- const rootId = "rosa-vis";
738
- const stepsId = "steps_json";
739
- const speedId = "speed_slider";
740
- const baseDelay = 650;
741
- const transferPauseMs = 200;
742
- const transferMs = 750;
743
- const debugAnim = true;
744
- let runToken = 0;
745
- let linkLayer = null;
746
- let activeTokenEl = null;
747
-
748
- function getAppRoot() {
749
- const app = document.querySelector("gradio-app");
750
- if (app && app.shadowRoot) return app.shadowRoot;
751
- return document;
752
- }
753
-
754
- function findInputById(id, selector) {
755
- const root = getAppRoot();
756
- const direct = root.querySelector(`#${id}`) || document.getElementById(id);
757
- if (!direct) return null;
758
- if (direct.matches && direct.matches(selector)) return direct;
759
- const nested = direct.querySelector(selector);
760
- if (nested) return nested;
761
- if (direct.shadowRoot) {
762
- const shadowEl = direct.shadowRoot.querySelector(selector);
763
- if (shadowEl) return shadowEl;
764
- }
765
- return null;
766
- }
767
-
768
- function getStepsBox() {
769
- return findInputById(stepsId, "textarea, input");
770
- }
771
-
772
- function getSpeed() {
773
- const slider = findInputById(speedId, 'input[type="range"], input');
774
- // If the speed slider isn't mounted yet (e.g. on first load), default to x2.
775
- const value = slider ? parseFloat(slider.value) : 2;
776
- if (!Number.isFinite(value) || value <= 0) return 1;
777
- return value;
778
- }
779
-
780
- function sleep(ms) {
781
- return new Promise((resolve) => setTimeout(resolve, ms));
782
- }
783
-
784
- function getOverlayHost() {
785
- const root = getAppRoot();
786
- if (root === document) return document.body;
787
- return root;
788
- }
789
-
790
- function animateTransfer(fromEl, toEl, value, durationMs, onFinish) {
791
- if (!fromEl || !toEl) {
792
- if (debugAnim) {
793
- console.warn("[ROSA] animateTransfer missing element", {
794
- hasFrom: !!fromEl,
795
- hasTo: !!toEl,
796
- });
797
- }
798
- return;
799
- }
800
- const fromRect = fromEl.getBoundingClientRect();
801
- const toRect = toEl.getBoundingClientRect();
802
- if (!fromRect || !toRect) {
803
- if (debugAnim) {
804
- console.warn("[ROSA] animateTransfer missing rect", {
805
- fromRect,
806
- toRect,
807
- });
808
- }
809
- return;
810
- }
811
- const bubble = document.createElement("div");
812
- bubble.className = "v-float";
813
- bubble.textContent = value != null ? String(value) : fromEl.textContent;
814
- bubble.style.position = "fixed";
815
- bubble.style.zIndex = "9999";
816
- bubble.style.borderRadius = "8px";
817
- bubble.style.background = "#f59e0b";
818
- bubble.style.color = "#1f2937";
819
- bubble.style.display = "flex";
820
- bubble.style.alignItems = "center";
821
- bubble.style.justifyContent = "center";
822
- bubble.style.fontWeight = "600";
823
- bubble.style.boxShadow = "0 10px 24px rgba(15, 23, 42, 0.2)";
824
- bubble.style.pointerEvents = "none";
825
- bubble.style.transform = "translate(0px, 0px) scale(1)";
826
- bubble.style.left = `${fromRect.left}px`;
827
- bubble.style.top = `${fromRect.top}px`;
828
- bubble.style.width = `${fromRect.width}px`;
829
- bubble.style.height = `${fromRect.height}px`;
830
- const host = getOverlayHost();
831
- if (debugAnim) {
832
- console.log("[ROSA] animateTransfer", {
833
- from: {
834
- left: fromRect.left,
835
- top: fromRect.top,
836
- width: fromRect.width,
837
- height: fromRect.height,
838
- },
839
- to: {
840
- left: toRect.left,
841
- top: toRect.top,
842
- width: toRect.width,
843
- height: toRect.height,
844
- },
845
- host: host === document.body ? "document.body" : "shadowRoot",
846
- value,
847
- });
848
- if (fromRect.width === 0 || fromRect.height === 0 || toRect.width === 0 || toRect.height === 0) {
849
- console.warn("[ROSA] animateTransfer zero rect", {
850
- fromRect,
851
- toRect,
852
- });
853
- }
854
- }
855
- host.appendChild(bubble);
856
- const dx = toRect.left - fromRect.left;
857
- const dy = toRect.top - fromRect.top;
858
- const toTransform = `translate(${dx}px, ${dy}px) scale(0.95)`;
859
- const duration =
860
- Number.isFinite(durationMs) && durationMs > 0 ? durationMs : transferMs;
861
- const startAnimation = () => {
862
- if (bubble.animate) {
863
- const anim = bubble.animate(
864
- [
865
- { transform: "translate(0px, 0px) scale(1)" },
866
- { transform: toTransform },
867
- ],
868
- { duration, easing: "ease-out", fill: "forwards" }
869
- );
870
- anim.addEventListener("finish", () => {
871
- if (onFinish) onFinish();
872
- bubble.remove();
873
- });
874
- return;
875
- }
876
- bubble.style.transition = `transform ${duration}ms ease`;
877
- bubble.style.willChange = "transform";
878
- requestAnimationFrame(() => {
879
- bubble.style.transform = toTransform;
880
- });
881
- setTimeout(() => {
882
- if (onFinish) onFinish();
883
- bubble.remove();
884
- }, duration + 80);
885
- };
886
- bubble.getBoundingClientRect();
887
- requestAnimationFrame(() => {
888
- requestAnimationFrame(startAnimation);
889
- });
890
- }
891
-
892
- function getCodeLines() {
893
- const root = getAppRoot();
894
- const container = root.querySelector("#rosa-code") || document.getElementById("rosa-code");
895
- if (!container) return {};
896
- const lines = {};
897
- container.querySelectorAll(".code-line").forEach((line) => {
898
- const index = parseInt(line.dataset.line, 10);
899
- if (Number.isFinite(index)) {
900
- lines[index] = line;
901
- }
902
- });
903
- return lines;
904
- }
905
-
906
- function resetCodeHighlight(codeLines) {
907
- Object.values(codeLines).forEach((line) => {
908
- line.classList.remove("active");
909
- });
910
- }
911
-
912
- function setActiveCodeLine(state, line) {
913
- if (!state.codeLines) return;
914
- if (Number.isInteger(state.activeLine) && state.codeLines[state.activeLine]) {
915
- state.codeLines[state.activeLine].classList.remove("active");
916
- }
917
- if (Number.isInteger(line) && state.codeLines[line]) {
918
- state.codeLines[line].classList.add("active");
919
- state.activeLine = line;
920
- }
921
- }
922
-
923
- function buildRow(label, values, className) {
924
- const row = document.createElement("div");
925
- row.className = `rosa-row ${className || ""}`;
926
- const labelEl = document.createElement("div");
927
- labelEl.className = "row-label";
928
- labelEl.textContent = label;
929
- const cellsEl = document.createElement("div");
930
- cellsEl.className = "row-cells";
931
- const cells = values.map((val, idx) => {
932
- const cell = document.createElement("div");
933
- cell.className = "cell";
934
- cell.textContent = String(val);
935
- cell.dataset.idx = String(idx);
936
- cellsEl.appendChild(cell);
937
- return cell;
938
- });
939
- row.appendChild(labelEl);
940
- row.appendChild(cellsEl);
941
- return { row, cells };
942
- }
943
-
944
- function getCodeToken(rootEl, tokenKey) {
945
- const root = rootEl || getAppRoot();
946
- return root.querySelector(`[data-token="${tokenKey}"]`);
947
- }
948
-
949
- function ensureLinkLayer(shellEl) {
950
- const host = shellEl || getAppRoot().querySelector("#rosa-shell") || document.body;
951
- if (linkLayer && linkLayer.host === host) return linkLayer;
952
- if (linkLayer && linkLayer.svg) {
953
- linkLayer.svg.remove();
954
- }
955
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
956
- svg.setAttribute("width", "100%");
957
- svg.setAttribute("height", "100%");
958
- svg.style.position = "absolute";
959
- svg.style.left = "0";
960
- svg.style.top = "0";
961
- svg.style.pointerEvents = "none";
962
- svg.style.zIndex = "10000";
963
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
964
- const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
965
- marker.setAttribute("id", "rosa-link-arrow");
966
- marker.setAttribute("markerWidth", "8");
967
- marker.setAttribute("markerHeight", "8");
968
- marker.setAttribute("refX", "6");
969
- marker.setAttribute("refY", "3");
970
- marker.setAttribute("orient", "auto");
971
- const markerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
972
- markerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
973
- markerPath.setAttribute("fill", "#94a3b8");
974
- marker.appendChild(markerPath);
975
- defs.appendChild(marker);
976
- svg.appendChild(defs);
977
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
978
- path.setAttribute("stroke", "#94a3b8");
979
- path.setAttribute("stroke-width", "1.5");
980
- path.setAttribute("fill", "none");
981
- path.setAttribute("marker-end", "url(#rosa-link-arrow)");
982
- path.style.display = "none";
983
- svg.appendChild(path);
984
- host.appendChild(svg);
985
- linkLayer = { svg, path, host };
986
- return linkLayer;
987
- }
988
-
989
- function showLink(state, fromEl, tokenKey) {
990
- if (!state || !state.shellEl) return;
991
- const tokenEl = getCodeToken(state.shellEl, tokenKey);
992
- if (!fromEl || !tokenEl) return;
993
- const shellRect = state.shellEl.getBoundingClientRect();
994
- const fromRect = fromEl.getBoundingClientRect();
995
- const toRect = tokenEl.getBoundingClientRect();
996
- const link = ensureLinkLayer(state.shellEl);
997
- let startX = fromRect.right - shellRect.left;
998
- let endX = toRect.left - shellRect.left;
999
- if (toRect.left < fromRect.left) {
1000
- startX = fromRect.left - shellRect.left;
1001
- endX = toRect.right - shellRect.left;
1002
- }
1003
- const startY = fromRect.top + fromRect.height / 2 - shellRect.top;
1004
- const endY = toRect.top + toRect.height / 2 - shellRect.top;
1005
- link.path.setAttribute("d", `M ${startX} ${startY} L ${endX} ${endY}`);
1006
- link.path.style.display = "block";
1007
- if (activeTokenEl && activeTokenEl !== tokenEl) {
1008
- activeTokenEl.classList.remove("active");
1009
- }
1010
- tokenEl.classList.add("active");
1011
- activeTokenEl = tokenEl;
1012
- }
1013
-
1014
- function hideLink() {
1015
- if (linkLayer) {
1016
- linkLayer.path.style.display = "none";
1017
- }
1018
- if (activeTokenEl) {
1019
- activeTokenEl.classList.remove("active");
1020
- activeTokenEl = null;
1021
- }
1022
- }
1023
-
1024
- function getRangeRect(cells, start, end) {
1025
- const rects = [];
1026
- for (let idx = start; idx <= end; idx += 1) {
1027
- const cell = cells[idx];
1028
- if (!cell) continue;
1029
- rects.push(cell.getBoundingClientRect());
1030
- }
1031
- if (!rects.length) return null;
1032
- let left = rects[0].left;
1033
- let right = rects[0].right;
1034
- let top = rects[0].top;
1035
- let bottom = rects[0].bottom;
1036
- rects.forEach((rect) => {
1037
- left = Math.min(left, rect.left);
1038
- right = Math.max(right, rect.right);
1039
- top = Math.min(top, rect.top);
1040
- bottom = Math.max(bottom, rect.bottom);
1041
- });
1042
- return {
1043
- left,
1044
- top,
1045
- right,
1046
- bottom,
1047
- width: right - left,
1048
- height: bottom - top,
1049
- };
1050
- }
1051
-
1052
- function toLocalRect(rect, containerRect) {
1053
- return {
1054
- left: rect.left - containerRect.left,
1055
- top: rect.top - containerRect.top,
1056
- width: rect.width,
1057
- height: rect.height,
1058
- right: rect.right - containerRect.left,
1059
- bottom: rect.bottom - containerRect.top,
1060
- };
1061
- }
1062
-
1063
- function padRect(rect, pad, bounds) {
1064
- const left = Math.max(0, rect.left - pad);
1065
- const top = Math.max(0, rect.top - pad);
1066
- const right = Math.min(bounds.width, rect.left + rect.width + pad);
1067
- const bottom = Math.min(bounds.height, rect.top + rect.height + pad);
1068
- return {
1069
- left,
1070
- top,
1071
- width: Math.max(0, right - left),
1072
- height: Math.max(0, bottom - top),
1073
- right,
1074
- bottom,
1075
- };
1076
- }
1077
-
1078
- function createOverlayBox(layer, rect, label, className, labelClass) {
1079
- const box = document.createElement("div");
1080
- box.className = `overlay-box ${className || ""}`;
1081
- box.style.left = `${rect.left}px`;
1082
- box.style.top = `${rect.top}px`;
1083
- box.style.width = `${rect.width}px`;
1084
- box.style.height = `${rect.height}px`;
1085
- const labelEl = document.createElement("div");
1086
- labelEl.className = `overlay-label ${labelClass || ""}`;
1087
- labelEl.textContent = label;
1088
- box.appendChild(labelEl);
1089
- box.dataset.label = label;
1090
- layer.appendChild(box);
1091
- }
1092
-
1093
- function clearHoverBoxes(state) {
1094
- if (state.overlayHover) {
1095
- state.overlayHover.innerHTML = "";
1096
- }
1097
- state.hoverBoxes = [];
1098
- hideLink();
1099
- }
1100
-
1101
- function clearOverlay(state) {
1102
- if (state.overlayBoxes) {
1103
- state.overlayBoxes.innerHTML = "";
1104
- }
1105
- if (state.rayLine) {
1106
- state.rayLine.style.display = "none";
1107
- }
1108
- clearHoverBoxes(state);
1109
- }
1110
-
1111
- function updateOverlay(state, step) {
1112
- const hasI = Number.isInteger(step.i);
1113
- const hasW = Number.isInteger(step.w);
1114
- const hasJ = Number.isInteger(step.j);
1115
- if (step.phase === "loop_i") {
1116
- state.lastOverlay = null;
1117
- }
1118
- const showOverlay = [
1119
- "loop_w",
1120
- "assign_t",
1121
- "loop_j",
1122
- "try",
1123
- "assign",
1124
- "break_inner",
1125
- "check",
1126
- "break_outer",
1127
- ].includes(step.phase);
1128
- let overlay = null;
1129
- if (hasI && hasW) {
1130
- const sameWindow =
1131
- state.lastOverlay &&
1132
- state.lastOverlay.i === step.i &&
1133
- state.lastOverlay.w === step.w;
1134
- const jValue = hasJ ? step.j : (sameWindow ? state.lastOverlay.j : null);
1135
- state.lastOverlay = { i: step.i, w: step.w, j: jValue };
1136
- overlay = state.lastOverlay;
1137
- } else if (state.lastOverlay) {
1138
- overlay = state.lastOverlay;
1139
- }
1140
- clearOverlay(state);
1141
- if (!showOverlay) {
1142
- return;
1143
- }
1144
- if (!overlay) return;
1145
- const tStart = overlay.i - overlay.w + 1;
1146
- const tEnd = overlay.i;
1147
- const kStart = overlay.j;
1148
- const kEnd = Number.isInteger(overlay.j) ? overlay.j + overlay.w - 1 : null;
1149
- const tRect = getRangeRect(state.qCells, tStart, tEnd);
1150
- const kRect =
1151
- Number.isInteger(kStart) && Number.isInteger(kEnd)
1152
- ? getRangeRect(state.kCells, kStart, kEnd)
1153
- : null;
1154
- const vIndex = Number.isInteger(kStart) ? kStart + overlay.w : null;
1155
- const vCell = Number.isInteger(vIndex) ? state.vCells[vIndex] : null;
1156
- const vRect = vCell ? vCell.getBoundingClientRect() : null;
1157
- const cardRect = state.cardEl.getBoundingClientRect();
1158
- const cardStyle = window.getComputedStyle(state.cardEl);
1159
- const borderLeft = parseFloat(cardStyle.borderLeftWidth) || 0;
1160
- const borderTop = parseFloat(cardStyle.borderTopWidth) || 0;
1161
- const borderRight = parseFloat(cardStyle.borderRightWidth) || 0;
1162
- const borderBottom = parseFloat(cardStyle.borderBottomWidth) || 0;
1163
- const originLeft = cardRect.left + borderLeft;
1164
- const originTop = cardRect.top + borderTop;
1165
- const boxPad = 4;
1166
- const bounds = {
1167
- width: cardRect.width - borderLeft - borderRight,
1168
- height: cardRect.height - borderTop - borderBottom,
1169
- };
1170
- const toOverlayRect = (rect) => ({
1171
- left: rect.left - originLeft,
1172
- top: rect.top - originTop,
1173
- width: rect.width,
1174
- height: rect.height,
1175
- right: rect.right - originLeft,
1176
- bottom: rect.bottom - originTop,
1177
- });
1178
- const isTry = step.phase === "try";
1179
- const tryClass = isTry ? (step.matched ? "try-match" : "try-miss") : "";
1180
-
1181
- if (tRect) {
1182
- const local = padRect(toOverlayRect(tRect), boxPad, bounds);
1183
- createOverlayBox(state.overlayBoxes, local, "t", `t-box ${tryClass}`, "t-label");
1184
- const hoverBox = document.createElement("div");
1185
- hoverBox.className = "overlay-hover-box";
1186
- hoverBox.style.left = `${local.left}px`;
1187
- hoverBox.style.top = `${local.top}px`;
1188
- hoverBox.style.width = `${local.width}px`;
1189
- hoverBox.style.height = `${local.height}px`;
1190
- hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "t"));
1191
- hoverBox.addEventListener("mouseleave", hideLink);
1192
- state.overlayHover.appendChild(hoverBox);
1193
- state.hoverBoxes.push(hoverBox);
1194
- }
1195
-
1196
- if (kRect) {
1197
- const local = padRect(toOverlayRect(kRect), boxPad, bounds);
1198
- createOverlayBox(state.overlayBoxes, local, "kkk[j:j+w]", `k-box ${tryClass}`, "k-label");
1199
- const hoverBox = document.createElement("div");
1200
- hoverBox.className = "overlay-hover-box";
1201
- hoverBox.style.left = `${local.left}px`;
1202
- hoverBox.style.top = `${local.top}px`;
1203
- hoverBox.style.width = `${local.width}px`;
1204
- hoverBox.style.height = `${local.height}px`;
1205
- hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "k"));
1206
- hoverBox.addEventListener("mouseleave", hideLink);
1207
- state.overlayHover.appendChild(hoverBox);
1208
- state.hoverBoxes.push(hoverBox);
1209
- }
1210
-
1211
- if (step.phase === "assign" && kRect && vRect && state.rayLine) {
1212
- const kLocal = padRect(toOverlayRect(kRect), boxPad, bounds);
1213
- const vLocal = toOverlayRect(vRect);
1214
- const startX = kLocal.right;
1215
- const startY = kLocal.bottom;
1216
- const endX = vLocal.left + vLocal.width / 2;
1217
- const endY = vLocal.top + vLocal.height / 2;
1218
- state.rayLine.setAttribute("x1", startX);
1219
- state.rayLine.setAttribute("y1", startY);
1220
- state.rayLine.setAttribute("x2", endX);
1221
- state.rayLine.setAttribute("y2", endY);
1222
- state.rayLine.style.display = "block";
1223
- }
1224
- }
1225
-
1226
- function render(data) {
1227
- const root = getAppRoot().querySelector(`#${rootId}`);
1228
- if (!root) return null;
1229
- root.innerHTML = "";
1230
-
1231
- const card = document.createElement("div");
1232
- card.className = "rosa-card";
1233
-
1234
- const legend = document.createElement("div");
1235
- legend.className = "rosa-legend";
1236
- legend.innerHTML = `
1237
- <span class="legend-item"><span class="legend-dot legend-suffix"></span>Current suffix (t)</span>
1238
- <span class="legend-item"><span class="legend-dot legend-window"></span>k window (kkk[j:j+w])</span>
1239
- <span class="legend-item"><span class="legend-dot legend-match"></span>Match (kkk[j:j+w]==t)</span>
1240
- <span class="legend-item"><span class="legend-dot legend-v"></span>Read v (vvv[j+w])</span>
1241
- <span class="legend-item"><span class="legend-dot legend-out"></span>Output (out)</span>
1242
- `;
1243
- card.appendChild(legend);
1244
-
1245
- const rowsWrap = document.createElement("div");
1246
- rowsWrap.className = "rosa-rows";
1247
-
1248
- const indexRow = document.createElement("div");
1249
- indexRow.className = "rosa-row index-row";
1250
- const indexLabel = document.createElement("div");
1251
- indexLabel.className = "row-label";
1252
- indexLabel.textContent = "#";
1253
- const indexCells = document.createElement("div");
1254
- indexCells.className = "row-cells index-cells";
1255
- data.q.forEach((_, idx) => {
1256
- const cell = document.createElement("div");
1257
- cell.className = "index-cell";
1258
- cell.textContent = String(idx);
1259
- indexCells.appendChild(cell);
1260
- });
1261
- indexRow.appendChild(indexLabel);
1262
- indexRow.appendChild(indexCells);
1263
- rowsWrap.appendChild(indexRow);
1264
-
1265
- const qRow = buildRow("q", data.q, "q-row");
1266
- const kRow = buildRow("k", data.k, "k-row");
1267
- const vRow = buildRow("v", data.v, "v-row");
1268
- const outRow = buildRow("out", data.q.map(() => "."), "out-row");
1269
-
1270
- rowsWrap.appendChild(qRow.row);
1271
- rowsWrap.appendChild(kRow.row);
1272
- rowsWrap.appendChild(vRow.row);
1273
- rowsWrap.appendChild(outRow.row);
1274
-
1275
- card.appendChild(rowsWrap);
1276
- const overlay = document.createElement("div");
1277
- overlay.className = "rosa-overlay";
1278
- const overlayRay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1279
- overlayRay.classList.add("overlay-ray");
1280
- const rayDefs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1281
- const rayMarker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
1282
- rayMarker.setAttribute("id", "rosa-ray-head");
1283
- rayMarker.setAttribute("markerWidth", "8");
1284
- rayMarker.setAttribute("markerHeight", "8");
1285
- rayMarker.setAttribute("refX", "6");
1286
- rayMarker.setAttribute("refY", "3");
1287
- rayMarker.setAttribute("orient", "auto");
1288
- const rayMarkerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
1289
- rayMarkerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
1290
- rayMarkerPath.setAttribute("fill", "var(--rosa-cyan)");
1291
- rayMarker.appendChild(rayMarkerPath);
1292
- rayDefs.appendChild(rayMarker);
1293
- overlayRay.appendChild(rayDefs);
1294
- const rayLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
1295
- rayLine.classList.add("overlay-ray-line");
1296
- rayLine.setAttribute("marker-end", "url(#rosa-ray-head)");
1297
- rayLine.style.display = "none";
1298
- overlayRay.appendChild(rayLine);
1299
- const overlayBoxes = document.createElement("div");
1300
- overlayBoxes.className = "overlay-box-layer";
1301
- overlay.appendChild(overlayBoxes);
1302
- overlay.appendChild(overlayRay);
1303
- card.appendChild(overlay);
1304
- const overlayHover = document.createElement("div");
1305
- overlayHover.className = "overlay-hover-layer";
1306
- card.appendChild(overlayHover);
1307
- root.appendChild(card);
1308
-
1309
- const codeLines = getCodeLines();
1310
- resetCodeHighlight(codeLines);
1311
-
1312
- return {
1313
- shellEl: root.closest("#rosa-shell") || getAppRoot().querySelector("#rosa-shell"),
1314
- cardEl: card,
1315
- qCells: qRow.cells,
1316
- kCells: kRow.cells,
1317
- vCells: vRow.cells,
1318
- outCells: outRow.cells,
1319
- outFixed: new Set(),
1320
- outPending: new Set(),
1321
- overlayBoxes,
1322
- rayLine,
1323
- overlayHover,
1324
- hoverBoxes: [],
1325
- lastOverlay: null,
1326
- codeLines,
1327
- activeLine: null,
1328
- };
1329
- }
1330
-
1331
- function clearHighlights(state) {
1332
- const holdActive =
1333
- state.vHold &&
1334
- Number.isInteger(state.vHold.index) &&
1335
- performance.now() < state.vHold.until;
1336
- const holdIndex = holdActive ? state.vHold.index : null;
1337
- const all = [...state.qCells, ...state.kCells, ...state.vCells, ...state.outCells];
1338
- all.forEach((cell) => {
1339
- cell.classList.remove("active", "suffix", "k-window", "v-pick", "out");
1340
- });
1341
- if (holdActive && state.vCells[holdIndex]) {
1342
- state.vCells[holdIndex].classList.add("v-pick");
1343
- }
1344
- }
1345
-
1346
- function applyStep(state, step) {
1347
- clearHighlights(state);
1348
- const { qCells, kCells, vCells, outCells } = state;
1349
- if (Number.isInteger(step.line)) {
1350
- setActiveCodeLine(state, step.line);
1351
- }
1352
-
1353
- const showWindow = [
1354
- "loop_w",
1355
- "assign_t",
1356
- "loop_j",
1357
- "try",
1358
- "assign",
1359
- "break_inner",
1360
- "check",
1361
- "break_outer",
1362
- ].includes(step.phase);
1363
- if (showWindow && Number.isInteger(step.i) && qCells[step.i]) {
1364
- qCells[step.i].classList.add("active");
1365
- }
1366
- if (showWindow && Number.isInteger(step.w)) {
1367
- for (let idx = step.i - step.w + 1; idx <= step.i; idx += 1) {
1368
- if (qCells[idx]) qCells[idx].classList.add("suffix");
1369
- }
1370
- }
1371
- if (showWindow && Number.isInteger(step.j)) {
1372
- for (let idx = step.j; idx <= step.j + step.w - 1; idx += 1) {
1373
- if (kCells[idx]) kCells[idx].classList.add("k-window");
1374
- }
1375
- }
1376
- updateOverlay(state, step);
1377
-
1378
- if (step.phase === "try") {
1379
- return;
1380
- }
1381
-
1382
- if (step.phase === "assign") {
1383
- if (debugAnim) {
1384
- console.log("[ROSA] assign step", {
1385
- i: step.i,
1386
- j: step.j,
1387
- w: step.w,
1388
- v_index: step.v_index,
1389
- value: step.value,
1390
- });
1391
- }
1392
- if (Number.isInteger(step.v_index) && vCells[step.v_index]) {
1393
- vCells[step.v_index].classList.add("v-pick");
1394
- }
1395
- if (
1396
- Number.isInteger(step.v_index) &&
1397
- Number.isInteger(step.i) &&
1398
- vCells[step.v_index] &&
1399
- outCells[step.i]
1400
- ) {
1401
- const speed = getSpeed();
1402
- const holdToken = state.runToken;
1403
- if (state.outPending) {
1404
- state.outPending.add(step.i);
1405
- }
1406
- const pauseMs = transferPauseMs / speed;
1407
- const durationMs = transferMs / speed;
1408
- const totalWait = pauseMs + durationMs;
1409
- if (state.outPending) {
1410
- state.outPending.add(step.i);
1411
- }
1412
- const startTransfer = () => {
1413
- if (state.runToken !== holdToken) return;
1414
- animateTransfer(vCells[step.v_index], outCells[step.i], step.value, durationMs, () => {
1415
- if (state.runToken !== holdToken) return;
1416
- if (state.outPending) {
1417
- state.outPending.delete(step.i);
1418
- }
1419
- if (state.outFixed) {
1420
- state.outFixed.add(step.i);
1421
- }
1422
- const outCell = outCells[step.i];
1423
- if (outCell) {
1424
- outCell.textContent = String(step.value);
1425
- outCell.classList.add("out-fixed", "filled");
1426
- }
1427
- });
1428
- };
1429
- if (pauseMs > 0) {
1430
- setTimeout(startTransfer, pauseMs);
1431
- } else {
1432
- startTransfer();
1433
- }
1434
- state.vHold = {
1435
- index: step.v_index,
1436
- until: performance.now() + totalWait,
1437
- };
1438
- const holdIndex = step.v_index;
1439
- setTimeout(() => {
1440
- if (state.runToken !== holdToken) return;
1441
- if (!state.vHold || state.vHold.index !== holdIndex) return;
1442
- if (performance.now() < state.vHold.until) return;
1443
- const cell = state.vCells[holdIndex];
1444
- if (cell) cell.classList.remove("v-pick");
1445
- }, totalWait + 60);
1446
- return totalWait;
1447
- }
1448
- return 0;
1449
- }
1450
-
1451
- if (step.phase === "break_inner") {
1452
- return;
1453
- }
1454
-
1455
- if (step.phase === "check") {
1456
- return;
1457
- }
1458
-
1459
- if (step.phase === "break_outer") {
1460
- return;
1461
- }
1462
-
1463
- if (step.phase === "output") {
1464
- if (
1465
- (state.outPending && state.outPending.has(step.i)) ||
1466
- (state.outFixed && state.outFixed.has(step.i))
1467
- ) {
1468
- return;
1469
- }
1470
- if (outCells[step.i]) {
1471
- outCells[step.i].textContent = String(step.value);
1472
- outCells[step.i].classList.add("out", "out-fixed", "filled");
1473
- if (state.outFixed) {
1474
- state.outFixed.add(step.i);
1475
- }
1476
- }
1477
- return;
1478
- }
1479
-
1480
- if (step.phase === "return") {
1481
- return;
1482
- }
1483
- }
1484
-
1485
- async function play(state, steps, token) {
1486
- if (!steps || !steps.length) return;
1487
- for (let idx = 0; idx < steps.length; idx += 1) {
1488
- if (token !== runToken) return;
1489
- const extraWait = applyStep(state, steps[idx]);
1490
- const delay = baseDelay / getSpeed();
1491
- const waitMs = Math.max(delay, Number.isFinite(extraWait) ? extraWait : 0);
1492
- await sleep(waitMs);
1493
- }
1494
- }
1495
-
1496
- function start(data) {
1497
- runToken += 1;
1498
- const token = runToken;
1499
- const state = render(data);
1500
- if (!state) return;
1501
- state.runToken = token;
1502
- state.vHold = null;
1503
- if (state.outFixed) state.outFixed.clear();
1504
- if (state.outPending) state.outPending.clear();
1505
- play(state, data.steps, token);
1506
- }
1507
-
1508
- let lastValue = "";
1509
- setInterval(() => {
1510
- const box = getStepsBox();
1511
- if (!box) return;
1512
- const value = box.value || "";
1513
- if (value && value !== lastValue) {
1514
- lastValue = value;
1515
- try {
1516
- const data = JSON.parse(value);
1517
- start(data);
1518
- } catch (err) {
1519
- console.error("Invalid ROSA steps payload", err);
1520
- }
1521
- }
1522
- }, 300);
1523
- }
1524
  """
1525
 
1526
- JS_BOOT = f"(function(){{ const init = {JS_FUNC}; init(); return init; }})()"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1527
 
 
 
1528
 
 
1529
  demo_context = gr.Blocks(css=CSS, js=JS_BOOT)
1530
 
1531
  with demo_context as demo:
 
1532
  gr.HTML(
1533
  '<div class="page-header">'
1534
  '<div class="page-title">RWKV-8 ROSA-QKV-1bit Demo</div>'
@@ -1536,11 +33,13 @@ with demo_context as demo:
1536
  "</div>"
1537
  )
1538
 
 
1539
  with gr.Row():
1540
  q_text = gr.Textbox(label="q sequence", value="01010101010101010101", lines=1)
1541
  k_text = gr.Textbox(label="k sequence", value="10100110100110100110", lines=1)
1542
  v_text = gr.Textbox(label="v sequence", value="11001100110011001100", lines=1)
1543
 
 
1544
  with gr.Row():
1545
  length = gr.Slider(4, 20, value=20, step=1, label="Random length")
1546
  random_btn = gr.Button("Randomize")
@@ -1554,10 +53,21 @@ with demo_context as demo:
1554
  elem_id="speed_slider",
1555
  interactive=True,
1556
  )
 
 
 
 
 
1557
 
 
1558
  out_text = gr.Textbox(label="Output", interactive=False)
1559
- steps_json = gr.Textbox(visible=False, elem_id="steps_json")
 
 
 
 
1560
 
 
1561
  gr.HTML(
1562
  f'<div id="rosa-shell" class="rosa-shell">'
1563
  f'<div class="rosa-pane"><div id="rosa-vis"></div></div>'
@@ -1571,9 +81,22 @@ with demo_context as demo:
1571
  f"</div>"
1572
  )
1573
 
 
1574
  random_btn.click(on_random, inputs=[length], outputs=[q_text, k_text, v_text])
1575
- demo_btn.click(on_demo, inputs=[q_text, k_text, v_text], outputs=[steps_json, out_text])
1576
- demo.load(on_demo, inputs=[q_text, k_text, v_text], outputs=[steps_json, out_text])
 
 
 
 
 
 
 
 
 
 
 
 
1577
 
1578
 
1579
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ ROSA-QKV-1bit 交互式演示应用 - 主程序
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
 
5
+ import gradio as gr
6
+ from constants import ROSA_CODE, ROSA_QUICK_CODE, GRADIO_MAJOR, MAX_DEMO_LEN
7
+ from code_highlighting import build_plain_code_html
8
+ from code_builder import build_code_html
9
+ from algorithm import initialize_line_numbers
10
+ from ui_styles import CSS
11
+ from javascript_handler import JS_FUNC, get_js_boot
12
+ from ui_handlers import on_demo, on_random
13
+
14
+ # 初始化行号映射
15
+ LINE_NUMBERS = initialize_line_numbers(ROSA_CODE)
16
+
17
+ # 构建代码 HTML
18
+ CODE_HTML = build_code_html(ROSA_CODE, LINE_NUMBERS)
19
+ QUICK_CODE_HTML = build_plain_code_html(ROSA_QUICK_CODE, "rosa-code-quick")
20
 
21
+ # 获取 JavaScript 启动代码
22
+ JS_BOOT = get_js_boot(JS_FUNC)
23
 
24
+ # 创建 Gradio 应用
25
  demo_context = gr.Blocks(css=CSS, js=JS_BOOT)
26
 
27
  with demo_context as demo:
28
+ # 页面标题
29
  gr.HTML(
30
  '<div class="page-header">'
31
  '<div class="page-title">RWKV-8 ROSA-QKV-1bit Demo</div>'
 
33
  "</div>"
34
  )
35
 
36
+ # 输入行
37
  with gr.Row():
38
  q_text = gr.Textbox(label="q sequence", value="01010101010101010101", lines=1)
39
  k_text = gr.Textbox(label="k sequence", value="10100110100110100110", lines=1)
40
  v_text = gr.Textbox(label="v sequence", value="11001100110011001100", lines=1)
41
 
42
+ # 控制行
43
  with gr.Row():
44
  length = gr.Slider(4, 20, value=20, step=1, label="Random length")
45
  random_btn = gr.Button("Randomize")
 
53
  elem_id="speed_slider",
54
  interactive=True,
55
  )
56
+ theme_toggle = gr.Checkbox(
57
+ label="Dark mode",
58
+ value=False,
59
+ elem_id="theme_toggle",
60
+ )
61
 
62
+ # 输出
63
  out_text = gr.Textbox(label="Output", interactive=False)
64
+ steps_json = gr.Textbox(
65
+ visible=True,
66
+ elem_id="steps_json",
67
+ elem_classes=["rosa-hidden"],
68
+ )
69
 
70
+ # 可视化和代码面板
71
  gr.HTML(
72
  f'<div id="rosa-shell" class="rosa-shell">'
73
  f'<div class="rosa-pane"><div id="rosa-vis"></div></div>'
 
81
  f"</div>"
82
  )
83
 
84
+ # 绑定事件处理器
85
  random_btn.click(on_random, inputs=[length], outputs=[q_text, k_text, v_text])
86
+
87
+ def on_demo_with_lines(q, k, v):
88
+ return on_demo(q, k, v, LINE_NUMBERS)
89
+
90
+ demo_btn.click(
91
+ on_demo_with_lines,
92
+ inputs=[q_text, k_text, v_text],
93
+ outputs=[steps_json, out_text],
94
+ )
95
+ demo.load(
96
+ on_demo_with_lines,
97
+ inputs=[q_text, k_text, v_text],
98
+ outputs=[steps_json, out_text],
99
+ )
100
 
101
 
102
  if __name__ == "__main__":
code_builder.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 代码 HTML 构建模块 - 生成带有行号和高亮的代码 HTML
3
+ """
4
+
5
+ import html
6
+ from code_highlighting import highlight_python_line
7
+
8
+
9
+ def build_code_html(code: str, line_numbers: dict) -> str:
10
+ """
11
+ 构建带有交互式标记的代码 HTML
12
+ 用于在演示中突出显示关键代码行和标记
13
+ """
14
+ lines = code.splitlines()
15
+ rendered = ['<div id="rosa-code" class="rosa-code">']
16
+ marker_t = "__TOK_T__"
17
+ marker_k = "__TOK_K__"
18
+ marker_v = "__TOK_V__"
19
+
20
+ for index, line in enumerate(lines, start=1):
21
+ line_with_markers = line
22
+ if index == line_numbers["LINE_TRY"]:
23
+ line_with_markers = line_with_markers.replace("kkk[j:j+w]", marker_k, 1)
24
+ line_with_markers = line_with_markers.replace("t", marker_t, 1)
25
+ if index == line_numbers["LINE_ASSIGN"]:
26
+ line_with_markers = line_with_markers.replace("vvv[j+w]", marker_v, 1)
27
+
28
+ highlighted = highlight_python_line(line_with_markers)
29
+ highlighted = highlighted.replace(
30
+ marker_t, '<span class="code-token" data-token="t">t</span>'
31
+ )
32
+ highlighted = highlighted.replace(
33
+ marker_k, '<span class="code-token" data-token="k">kkk[j:j+w]</span>'
34
+ )
35
+ highlighted = highlighted.replace(
36
+ marker_v, '<span class="code-token" data-token="v">vvv[j+w]</span>'
37
+ )
38
+
39
+ rendered.append(
40
+ '<div class="code-line" data-line="{line}">'
41
+ '<span class="line-no">{line}</span>'
42
+ '<span class="line-text">{text}</span>'
43
+ "</div>".format(line=index, text=highlighted)
44
+ )
45
+ rendered.append("</div>")
46
+ return "\n".join(rendered)
code_highlighting.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 代码高亮功能模块 - 处理 Python 代码的语法高亮
3
+ """
4
+
5
+ import html
6
+ import re
7
+ from constants import KEYWORD_RE, BUILTIN_RE, NUMBER_RE
8
+
9
+
10
+ def split_comment(line: str) -> tuple[str, str]:
11
+ """分离代码行中的注释部分"""
12
+ in_single = False
13
+ in_double = False
14
+ escaped = False
15
+ for index, ch in enumerate(line):
16
+ if escaped:
17
+ escaped = False
18
+ continue
19
+ if ch == "\\":
20
+ escaped = True
21
+ continue
22
+ if ch == "'" and not in_double:
23
+ in_single = not in_single
24
+ continue
25
+ if ch == '"' and not in_single:
26
+ in_double = not in_double
27
+ continue
28
+ if ch == "#" and not in_single and not in_double:
29
+ return line[:index], line[index:]
30
+ return line, ""
31
+
32
+
33
+ def tokenize_strings(code: str) -> list[tuple[str, str]]:
34
+ """将代码分解为字符串和文本段"""
35
+ segments: list[tuple[str, str]] = []
36
+ index = 0
37
+ while index < len(code):
38
+ ch = code[index]
39
+ if ch in ("'", '"'):
40
+ quote = ch
41
+ start = index
42
+ index += 1
43
+ escaped = False
44
+ while index < len(code):
45
+ if escaped:
46
+ escaped = False
47
+ index += 1
48
+ continue
49
+ if code[index] == "\\":
50
+ escaped = True
51
+ index += 1
52
+ continue
53
+ if code[index] == quote:
54
+ index += 1
55
+ break
56
+ index += 1
57
+ segments.append(("string", code[start:index]))
58
+ continue
59
+ start = index
60
+ while index < len(code) and code[index] not in ("'", '"'):
61
+ index += 1
62
+ segments.append(("text", code[start:index]))
63
+ return segments
64
+
65
+
66
+ def highlight_text_segment(text: str) -> str:
67
+ """高亮文本段中的关键字、内置函数和数字"""
68
+ escaped = html.escape(text)
69
+ escaped = KEYWORD_RE.sub(r'<span class="tok-keyword">\1</span>', escaped)
70
+ escaped = BUILTIN_RE.sub(r'<span class="tok-builtin">\1</span>', escaped)
71
+ escaped = NUMBER_RE.sub(r'<span class="tok-number">\1</span>', escaped)
72
+ return escaped
73
+
74
+
75
+ def highlight_python_line(line: str) -> str:
76
+ """高亮单行 Python 代码"""
77
+ code, comment = split_comment(line)
78
+ segments = tokenize_strings(code)
79
+ rendered: list[str] = []
80
+ for kind, text in segments:
81
+ if kind == "string":
82
+ rendered.append('<span class="tok-string">{}</span>'.format(html.escape(text)))
83
+ else:
84
+ rendered.append(highlight_text_segment(text))
85
+ if comment:
86
+ rendered.append('<span class="tok-comment">{}</span>'.format(html.escape(comment)))
87
+ return "".join(rendered)
88
+
89
+
90
+ def build_plain_code_html(code: str, block_id: str) -> str:
91
+ """构建简单的代码 HTML 块"""
92
+ lines = code.splitlines()
93
+ rendered = [f'<div id="{block_id}" class="rosa-code">']
94
+ for index, line in enumerate(lines, start=1):
95
+ highlighted = highlight_python_line(line)
96
+ rendered.append(
97
+ '<div class="code-line">'
98
+ '<span class="line-no">{line}</span>'
99
+ '<span class="line-text">{text}</span>'
100
+ "</div>".format(line=index, text=highlighted)
101
+ )
102
+ rendered.append("</div>")
103
+ return "\n".join(rendered)
constants.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 常量定义 - ROSA-QKV-1bit 演示应用
3
+ """
4
+
5
+ import re
6
+ import gradio as gr
7
+
8
+ # 演示配置
9
+ MAX_DEMO_LEN = 20
10
+ GRADIO_MAJOR = int((gr.__version__ or "0").split(".", maxsplit=1)[0])
11
+
12
+ # 算法代码示例
13
+ ROSA_CODE = """
14
+ def rosa_qkv_naive(qqq, kkk, vvv):
15
+ n=len(qqq); out=[-1]*n
16
+ for i in range(n):
17
+ for w in range(i+1,0,-1):
18
+ t=qqq[i+1-w:i+1]
19
+ for j in range(i-w,-1,-1):
20
+ if kkk[j:j+w]==t:
21
+ out[i]=vvv[j+w]
22
+ break
23
+ if out[i]!=-1:
24
+ break
25
+ return out
26
+ """.strip(
27
+ "\n"
28
+ )
29
+
30
+ ROSA_QUICK_CODE = """
31
+ def rosa_qkv_ref_minus1(qqq, kkk, vvv): # note: input will never contain "-1"
32
+ n=len(qqq); y=[-1]*n; s=2*n+1; t=[None]*s; f=[-1]*s; m=[0]*s; r=[-1]*s; t[0]={}; g=0; u=1; w=h=0; assert n==len(kkk)==len(vvv)
33
+ for i,(q,k) in enumerate(zip(qqq,kkk)):
34
+ p,x=w,h
35
+ while p!=-1 and q not in t[p]: x=m[p] if x>m[p] else x; p=f[p]
36
+ p,x=(t[p][q],x+1) if p!=-1 else (0,0); v=p
37
+ while f[v]!=-1 and m[f[v]]>=x: v=f[v]
38
+ while v!=-1 and (m[v]<=0 or r[v]<0): v=f[v]
39
+ y[i]=vvv[r[v]+1] if v!=-1 else -1; w,h=p,x; j=u; u+=1; t[j]={}; m[j]=m[g]+1; p=g
40
+ while p!=-1 and k not in t[p]: t[p][k]=j; p=f[p]
41
+ if p==-1: f[j]=0
42
+ else:
43
+ d=t[p][k]
44
+ if m[p]+1==m[d]: f[j]=d
45
+ else:
46
+ b=u; u+=1; t[b]=t[d].copy(); m[b]=m[p]+1; f[b]=f[d]; r[b]=r[d]; f[d]=f[j]=b
47
+ while p!=-1 and t[p][k]==d: t[p][k]=b; p=f[p]
48
+ v=g=j
49
+ while v!=-1 and r[v]<i: r[v]=i; v=f[v]
50
+ return y
51
+ """.strip(
52
+ "\n"
53
+ )
54
+
55
+ # Python 关键字和内置函数
56
+ KEYWORDS = {
57
+ "def",
58
+ "for",
59
+ "in",
60
+ "if",
61
+ "else",
62
+ "while",
63
+ "break",
64
+ "return",
65
+ "assert",
66
+ "None",
67
+ "True",
68
+ "False",
69
+ }
70
+
71
+ BUILTINS = {"len", "range", "zip", "enumerate"}
72
+
73
+ # 代码高亮的正则表达式
74
+ KEYWORD_RE = re.compile(r"\b(" + "|".join(sorted(KEYWORDS)) + r")\b")
75
+ BUILTIN_RE = re.compile(r"\b(" + "|".join(sorted(BUILTINS)) + r")\b")
76
+ NUMBER_RE = re.compile(r"(?<![\w.])(-?\d+)(?![\w.])")
javascript_handler.py ADDED
@@ -0,0 +1,880 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JavaScript 处理模块 - 前端交互逻辑
3
+ """
4
+
5
+ # 主要的 JavaScript 函数定义
6
+ JS_FUNC = """
7
+ () => {
8
+ if (window.__rosaDemoInit) return;
9
+ window.__rosaDemoInit = true;
10
+
11
+ const rootId = "rosa-vis";
12
+ const stepsId = "steps_json";
13
+ const speedId = "speed_slider";
14
+ const baseDelay = 650;
15
+ const transferPauseMs = 200;
16
+ const transferMs = 750;
17
+ const debugAnim = true;
18
+ let runToken = 0;
19
+ let linkLayer = null;
20
+ let activeTokenEl = null;
21
+ const themeToggleId = "theme_toggle";
22
+ const themeClass = "rosa-theme-dark";
23
+ const themeStorageKey = "rosa-theme";
24
+
25
+ function getAppRoot() {
26
+ const app = document.querySelector("gradio-app");
27
+ if (app && app.shadowRoot) return app.shadowRoot;
28
+ return document;
29
+ }
30
+
31
+ function findInputById(id, selector) {
32
+ const root = getAppRoot();
33
+ const direct = root.querySelector(`#${id}`) || document.getElementById(id);
34
+ if (!direct) return null;
35
+ if (direct.matches && direct.matches(selector)) return direct;
36
+ const nested = direct.querySelector(selector);
37
+ if (nested) return nested;
38
+ if (direct.shadowRoot) {
39
+ const shadowEl = direct.shadowRoot.querySelector(selector);
40
+ if (shadowEl) return shadowEl;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function getStepsBox() {
46
+ return findInputById(stepsId, "textarea, input");
47
+ }
48
+
49
+ function applyThemeClass(isDark) {
50
+ const root = getAppRoot();
51
+ const container = root.querySelector(".gradio-container");
52
+ const host = root.host || document.documentElement;
53
+ [container, host, document.documentElement, document.body].forEach((el) => {
54
+ if (!el || !el.classList) return;
55
+ el.classList.toggle(themeClass, isDark);
56
+ });
57
+ }
58
+
59
+ function getSystemDarkPreference() {
60
+ return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
61
+ }
62
+
63
+ function resolveInitialTheme() {
64
+ try {
65
+ const persisted = window.localStorage ? localStorage.getItem(themeStorageKey) : null;
66
+ if (persisted === "dark") return true;
67
+ if (persisted === "light") return false;
68
+ } catch (err) {
69
+ console.warn("[ROSA] theme localStorage unavailable", err);
70
+ }
71
+ // 没有持久化的选择,跟随系统
72
+ return getSystemDarkPreference();
73
+ }
74
+
75
+ function initThemeToggle() {
76
+ const isDark = resolveInitialTheme();
77
+ applyThemeClass(isDark);
78
+ const toggle = findInputById(themeToggleId, "input[type='checkbox']");
79
+ if (!toggle) return;
80
+ if (!toggle.dataset.rosaThemeBound) {
81
+ toggle.dataset.rosaThemeBound = "1";
82
+ toggle.addEventListener("change", () => {
83
+ const nextDark = !!toggle.checked;
84
+ applyThemeClass(nextDark);
85
+ try {
86
+ if (window.localStorage) {
87
+ localStorage.setItem(themeStorageKey, nextDark ? "dark" : "light");
88
+ }
89
+ } catch (err) {
90
+ console.warn("[ROSA] theme localStorage write failed", err);
91
+ }
92
+ });
93
+
94
+ // 监听系统主题变化,如果用户没有手动选择过则自动跟随
95
+ if (window.matchMedia) {
96
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
97
+ try {
98
+ const persisted = window.localStorage ? localStorage.getItem(themeStorageKey) : null;
99
+ // 只有在没有手动选择过的情况下才跟随系统
100
+ if (!persisted) {
101
+ const sysDark = e.matches;
102
+ applyThemeClass(sysDark);
103
+ toggle.checked = sysDark;
104
+ }
105
+ } catch (err) {
106
+ console.warn("[ROSA] system theme change handler failed", err);
107
+ }
108
+ });
109
+ }
110
+ }
111
+ toggle.checked = isDark;
112
+ }
113
+
114
+ function safeInitThemeToggle() {
115
+ try {
116
+ initThemeToggle();
117
+ } catch (err) {
118
+ console.warn("[ROSA] theme init failed", err);
119
+ }
120
+ }
121
+
122
+ function getSpeed() {
123
+ const slider = findInputById(speedId, 'input[type="range"], input');
124
+ const value = slider ? parseFloat(slider.value) : 2;
125
+ if (!Number.isFinite(value) || value <= 0) return 1;
126
+ return value;
127
+ }
128
+
129
+ function sleep(ms) {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+
133
+ function getOverlayHost() {
134
+ const root = getAppRoot();
135
+ if (root === document) return document.body;
136
+ return root;
137
+ }
138
+
139
+ function animateTransfer(fromEl, toEl, value, durationMs, onFinish) {
140
+ if (!fromEl || !toEl) {
141
+ if (debugAnim) {
142
+ console.warn("[ROSA] animateTransfer missing element", {
143
+ hasFrom: !!fromEl,
144
+ hasTo: !!toEl,
145
+ });
146
+ }
147
+ return;
148
+ }
149
+ const fromRect = fromEl.getBoundingClientRect();
150
+ const toRect = toEl.getBoundingClientRect();
151
+ if (!fromRect || !toRect) {
152
+ if (debugAnim) {
153
+ console.warn("[ROSA] animateTransfer missing rect", {
154
+ fromRect,
155
+ toRect,
156
+ });
157
+ }
158
+ return;
159
+ }
160
+ const bubble = document.createElement("div");
161
+ bubble.className = "v-float";
162
+ bubble.textContent = value != null ? String(value) : fromEl.textContent;
163
+ bubble.style.position = "fixed";
164
+ bubble.style.zIndex = "9999";
165
+ bubble.style.borderRadius = "8px";
166
+ bubble.style.background = "#f59e0b";
167
+ bubble.style.color = "#1f2937";
168
+ bubble.style.display = "flex";
169
+ bubble.style.alignItems = "center";
170
+ bubble.style.justifyContent = "center";
171
+ bubble.style.fontWeight = "600";
172
+ bubble.style.boxShadow = "0 10px 24px rgba(15, 23, 42, 0.2)";
173
+ bubble.style.pointerEvents = "none";
174
+ bubble.style.transform = "translate(0px, 0px) scale(1)";
175
+ bubble.style.left = `${fromRect.left}px`;
176
+ bubble.style.top = `${fromRect.top}px`;
177
+ bubble.style.width = `${fromRect.width}px`;
178
+ bubble.style.height = `${fromRect.height}px`;
179
+ const host = getOverlayHost();
180
+ if (debugAnim) {
181
+ console.log("[ROSA] animateTransfer", {
182
+ from: {
183
+ left: fromRect.left,
184
+ top: fromRect.top,
185
+ width: fromRect.width,
186
+ height: fromRect.height,
187
+ },
188
+ to: {
189
+ left: toRect.left,
190
+ top: toRect.top,
191
+ width: toRect.width,
192
+ height: toRect.height,
193
+ },
194
+ host: host === document.body ? "document.body" : "shadowRoot",
195
+ value,
196
+ });
197
+ if (fromRect.width === 0 || fromRect.height === 0 || toRect.width === 0 || toRect.height === 0) {
198
+ console.warn("[ROSA] animateTransfer zero rect", {
199
+ fromRect,
200
+ toRect,
201
+ });
202
+ }
203
+ }
204
+ host.appendChild(bubble);
205
+ const dx = toRect.left - fromRect.left;
206
+ const dy = toRect.top - fromRect.top;
207
+ const toTransform = `translate(${dx}px, ${dy}px) scale(0.95)`;
208
+ const duration =
209
+ Number.isFinite(durationMs) && durationMs > 0 ? durationMs : transferMs;
210
+ const startAnimation = () => {
211
+ if (bubble.animate) {
212
+ const anim = bubble.animate(
213
+ [
214
+ { transform: "translate(0px, 0px) scale(1)" },
215
+ { transform: toTransform },
216
+ ],
217
+ { duration, easing: "ease-out", fill: "forwards" }
218
+ );
219
+ anim.addEventListener("finish", () => {
220
+ if (onFinish) onFinish();
221
+ bubble.remove();
222
+ });
223
+ return;
224
+ }
225
+ bubble.style.transition = `transform ${duration}ms ease`;
226
+ bubble.style.willChange = "transform";
227
+ requestAnimationFrame(() => {
228
+ bubble.style.transform = toTransform;
229
+ });
230
+ setTimeout(() => {
231
+ if (onFinish) onFinish();
232
+ bubble.remove();
233
+ }, duration + 80);
234
+ };
235
+ bubble.getBoundingClientRect();
236
+ requestAnimationFrame(() => {
237
+ requestAnimationFrame(startAnimation);
238
+ });
239
+ }
240
+
241
+ function getCodeLines() {
242
+ const root = getAppRoot();
243
+ const container = root.querySelector("#rosa-code") || document.getElementById("rosa-code");
244
+ if (!container) return {};
245
+ const lines = {};
246
+ container.querySelectorAll(".code-line").forEach((line) => {
247
+ const index = parseInt(line.dataset.line, 10);
248
+ if (Number.isFinite(index)) {
249
+ lines[index] = line;
250
+ }
251
+ });
252
+ return lines;
253
+ }
254
+
255
+ function resetCodeHighlight(codeLines) {
256
+ Object.values(codeLines).forEach((line) => {
257
+ line.classList.remove("active");
258
+ });
259
+ }
260
+
261
+ function setActiveCodeLine(state, line) {
262
+ if (!state.codeLines) return;
263
+ if (Number.isInteger(state.activeLine) && state.codeLines[state.activeLine]) {
264
+ state.codeLines[state.activeLine].classList.remove("active");
265
+ }
266
+ if (Number.isInteger(line) && state.codeLines[line]) {
267
+ state.codeLines[line].classList.add("active");
268
+ state.activeLine = line;
269
+ }
270
+ }
271
+
272
+ function buildRow(label, values, className) {
273
+ const row = document.createElement("div");
274
+ row.className = `rosa-row ${className || ""}`;
275
+ const labelEl = document.createElement("div");
276
+ labelEl.className = "row-label";
277
+ labelEl.textContent = label;
278
+ const cellsEl = document.createElement("div");
279
+ cellsEl.className = "row-cells";
280
+ const cells = values.map((val, idx) => {
281
+ const cell = document.createElement("div");
282
+ cell.className = "cell";
283
+ cell.textContent = String(val);
284
+ cell.dataset.idx = String(idx);
285
+ cellsEl.appendChild(cell);
286
+ return cell;
287
+ });
288
+ row.appendChild(labelEl);
289
+ row.appendChild(cellsEl);
290
+ return { row, cells };
291
+ }
292
+
293
+ function getCodeToken(rootEl, tokenKey) {
294
+ const root = rootEl || getAppRoot();
295
+ return root.querySelector(`[data-token="${tokenKey}"]`);
296
+ }
297
+
298
+ function ensureLinkLayer(shellEl) {
299
+ const host = shellEl || getAppRoot().querySelector("#rosa-shell") || document.body;
300
+ if (linkLayer && linkLayer.host === host) return linkLayer;
301
+ if (linkLayer && linkLayer.svg) {
302
+ linkLayer.svg.remove();
303
+ }
304
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
305
+ svg.setAttribute("width", "100%");
306
+ svg.setAttribute("height", "100%");
307
+ svg.style.position = "absolute";
308
+ svg.style.left = "0";
309
+ svg.style.top = "0";
310
+ svg.style.pointerEvents = "none";
311
+ svg.style.zIndex = "10000";
312
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
313
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
314
+ marker.setAttribute("id", "rosa-link-arrow");
315
+ marker.setAttribute("markerWidth", "8");
316
+ marker.setAttribute("markerHeight", "8");
317
+ marker.setAttribute("refX", "6");
318
+ marker.setAttribute("refY", "3");
319
+ marker.setAttribute("orient", "auto");
320
+ const markerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
321
+ markerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
322
+ markerPath.setAttribute("fill", "#94a3b8");
323
+ marker.appendChild(markerPath);
324
+ defs.appendChild(marker);
325
+ svg.appendChild(defs);
326
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
327
+ path.setAttribute("stroke", "#94a3b8");
328
+ path.setAttribute("stroke-width", "1.5");
329
+ path.setAttribute("fill", "none");
330
+ path.setAttribute("marker-end", "url(#rosa-link-arrow)");
331
+ path.style.display = "none";
332
+ svg.appendChild(path);
333
+ host.appendChild(svg);
334
+ linkLayer = { svg, path, host };
335
+ return linkLayer;
336
+ }
337
+
338
+ function showLink(state, fromEl, tokenKey) {
339
+ if (!state || !state.shellEl) return;
340
+ const tokenEl = getCodeToken(state.shellEl, tokenKey);
341
+ if (!fromEl || !tokenEl) return;
342
+ const shellRect = state.shellEl.getBoundingClientRect();
343
+ const fromRect = fromEl.getBoundingClientRect();
344
+ const toRect = tokenEl.getBoundingClientRect();
345
+ const link = ensureLinkLayer(state.shellEl);
346
+ let startX = fromRect.right - shellRect.left;
347
+ let endX = toRect.left - shellRect.left;
348
+ if (toRect.left < fromRect.left) {
349
+ startX = fromRect.left - shellRect.left;
350
+ endX = toRect.right - shellRect.left;
351
+ }
352
+ const startY = fromRect.top + fromRect.height / 2 - shellRect.top;
353
+ const endY = toRect.top + toRect.height / 2 - shellRect.top;
354
+ link.path.setAttribute("d", `M ${startX} ${startY} L ${endX} ${endY}`);
355
+ link.path.style.display = "block";
356
+ if (activeTokenEl && activeTokenEl !== tokenEl) {
357
+ activeTokenEl.classList.remove("active");
358
+ }
359
+ tokenEl.classList.add("active");
360
+ activeTokenEl = tokenEl;
361
+ }
362
+
363
+ function hideLink() {
364
+ if (linkLayer) {
365
+ linkLayer.path.style.display = "none";
366
+ }
367
+ if (activeTokenEl) {
368
+ activeTokenEl.classList.remove("active");
369
+ activeTokenEl = null;
370
+ }
371
+ }
372
+
373
+ function getRangeRect(cells, start, end) {
374
+ const rects = [];
375
+ for (let idx = start; idx <= end; idx += 1) {
376
+ const cell = cells[idx];
377
+ if (!cell) continue;
378
+ rects.push(cell.getBoundingClientRect());
379
+ }
380
+ if (!rects.length) return null;
381
+ let left = rects[0].left;
382
+ let right = rects[0].right;
383
+ let top = rects[0].top;
384
+ let bottom = rects[0].bottom;
385
+ rects.forEach((rect) => {
386
+ left = Math.min(left, rect.left);
387
+ right = Math.max(right, rect.right);
388
+ top = Math.min(top, rect.top);
389
+ bottom = Math.max(bottom, rect.bottom);
390
+ });
391
+ return {
392
+ left,
393
+ top,
394
+ right,
395
+ bottom,
396
+ width: right - left,
397
+ height: bottom - top,
398
+ };
399
+ }
400
+
401
+ function toLocalRect(rect, containerRect) {
402
+ return {
403
+ left: rect.left - containerRect.left,
404
+ top: rect.top - containerRect.top,
405
+ width: rect.width,
406
+ height: rect.height,
407
+ right: rect.right - containerRect.left,
408
+ bottom: rect.bottom - containerRect.top,
409
+ };
410
+ }
411
+
412
+ function padRect(rect, pad, bounds) {
413
+ const left = Math.max(0, rect.left - pad);
414
+ const top = Math.max(0, rect.top - pad);
415
+ const right = Math.min(bounds.width, rect.left + rect.width + pad);
416
+ const bottom = Math.min(bounds.height, rect.top + rect.height + pad);
417
+ return {
418
+ left,
419
+ top,
420
+ width: Math.max(0, right - left),
421
+ height: Math.max(0, bottom - top),
422
+ right,
423
+ bottom,
424
+ };
425
+ }
426
+
427
+ function createOverlayBox(layer, rect, label, className, labelClass) {
428
+ const box = document.createElement("div");
429
+ box.className = `overlay-box ${className || ""}`;
430
+ box.style.left = `${rect.left}px`;
431
+ box.style.top = `${rect.top}px`;
432
+ box.style.width = `${rect.width}px`;
433
+ box.style.height = `${rect.height}px`;
434
+ const labelEl = document.createElement("div");
435
+ labelEl.className = `overlay-label ${labelClass || ""}`;
436
+ labelEl.textContent = label;
437
+ box.appendChild(labelEl);
438
+ box.dataset.label = label;
439
+ layer.appendChild(box);
440
+ }
441
+
442
+ function clearHoverBoxes(state) {
443
+ if (state.overlayHover) {
444
+ state.overlayHover.innerHTML = "";
445
+ }
446
+ state.hoverBoxes = [];
447
+ hideLink();
448
+ }
449
+
450
+ function clearOverlay(state) {
451
+ if (state.overlayBoxes) {
452
+ state.overlayBoxes.innerHTML = "";
453
+ }
454
+ if (state.rayLine) {
455
+ state.rayLine.style.display = "none";
456
+ }
457
+ clearHoverBoxes(state);
458
+ }
459
+
460
+ function updateOverlay(state, step) {
461
+ const hasI = Number.isInteger(step.i);
462
+ const hasW = Number.isInteger(step.w);
463
+ const hasJ = Number.isInteger(step.j);
464
+ if (step.phase === "loop_i") {
465
+ state.lastOverlay = null;
466
+ }
467
+ const showOverlay = [
468
+ "loop_w",
469
+ "assign_t",
470
+ "loop_j",
471
+ "try",
472
+ "assign",
473
+ "break_inner",
474
+ "check",
475
+ "break_outer",
476
+ ].includes(step.phase);
477
+ let overlay = null;
478
+ if (hasI && hasW) {
479
+ const sameWindow =
480
+ state.lastOverlay &&
481
+ state.lastOverlay.i === step.i &&
482
+ state.lastOverlay.w === step.w;
483
+ const jValue = hasJ ? step.j : (sameWindow ? state.lastOverlay.j : null);
484
+ state.lastOverlay = { i: step.i, w: step.w, j: jValue };
485
+ overlay = state.lastOverlay;
486
+ } else if (state.lastOverlay) {
487
+ overlay = state.lastOverlay;
488
+ }
489
+ clearOverlay(state);
490
+ if (!showOverlay) {
491
+ return;
492
+ }
493
+ if (!overlay) return;
494
+ const tStart = overlay.i - overlay.w + 1;
495
+ const tEnd = overlay.i;
496
+ const kStart = overlay.j;
497
+ const kEnd = Number.isInteger(overlay.j) ? overlay.j + overlay.w - 1 : null;
498
+ const tRect = getRangeRect(state.qCells, tStart, tEnd);
499
+ const kRect =
500
+ Number.isInteger(kStart) && Number.isInteger(kEnd)
501
+ ? getRangeRect(state.kCells, kStart, kEnd)
502
+ : null;
503
+ const vIndex = Number.isInteger(kStart) ? kStart + overlay.w : null;
504
+ const vCell = Number.isInteger(vIndex) ? state.vCells[vIndex] : null;
505
+ const vRect = vCell ? vCell.getBoundingClientRect() : null;
506
+ const cardRect = state.cardEl.getBoundingClientRect();
507
+ const cardStyle = window.getComputedStyle(state.cardEl);
508
+ const borderLeft = parseFloat(cardStyle.borderLeftWidth) || 0;
509
+ const borderTop = parseFloat(cardStyle.borderTopWidth) || 0;
510
+ const borderRight = parseFloat(cardStyle.borderRightWidth) || 0;
511
+ const borderBottom = parseFloat(cardStyle.borderBottomHeight) || 0;
512
+ const originLeft = cardRect.left + borderLeft;
513
+ const originTop = cardRect.top + borderTop;
514
+ const boxPad = 4;
515
+ const bounds = {
516
+ width: cardRect.width - borderLeft - borderRight,
517
+ height: cardRect.height - borderTop - borderBottom,
518
+ };
519
+ const toOverlayRect = (rect) => ({
520
+ left: rect.left - originLeft,
521
+ top: rect.top - originTop,
522
+ width: rect.width,
523
+ height: rect.height,
524
+ right: rect.right - originLeft,
525
+ bottom: rect.bottom - originTop,
526
+ });
527
+ const isTry = step.phase === "try";
528
+ const tryClass = isTry ? (step.matched ? "try-match" : "try-miss") : "";
529
+
530
+ if (tRect) {
531
+ const local = padRect(toOverlayRect(tRect), boxPad, bounds);
532
+ createOverlayBox(state.overlayBoxes, local, "t", `t-box ${tryClass}`, "t-label");
533
+ const hoverBox = document.createElement("div");
534
+ hoverBox.className = "overlay-hover-box";
535
+ hoverBox.style.left = `${local.left}px`;
536
+ hoverBox.style.top = `${local.top}px`;
537
+ hoverBox.style.width = `${local.width}px`;
538
+ hoverBox.style.height = `${local.height}px`;
539
+ hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "t"));
540
+ hoverBox.addEventListener("mouseleave", hideLink);
541
+ state.overlayHover.appendChild(hoverBox);
542
+ state.hoverBoxes.push(hoverBox);
543
+ }
544
+
545
+ if (kRect) {
546
+ const local = padRect(toOverlayRect(kRect), boxPad, bounds);
547
+ createOverlayBox(state.overlayBoxes, local, "kkk[j:j+w]", `k-box ${tryClass}`, "k-label");
548
+ const hoverBox = document.createElement("div");
549
+ hoverBox.className = "overlay-hover-box";
550
+ hoverBox.style.left = `${local.left}px`;
551
+ hoverBox.style.top = `${local.top}px`;
552
+ hoverBox.style.width = `${local.width}px`;
553
+ hoverBox.style.height = `${local.height}px`;
554
+ hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "k"));
555
+ hoverBox.addEventListener("mouseleave", hideLink);
556
+ state.overlayHover.appendChild(hoverBox);
557
+ state.hoverBoxes.push(hoverBox);
558
+ }
559
+
560
+ if (step.phase === "assign" && kRect && vRect && state.rayLine) {
561
+ const kLocal = padRect(toOverlayRect(kRect), boxPad, bounds);
562
+ const vLocal = toOverlayRect(vRect);
563
+ const startX = kLocal.right;
564
+ const startY = kLocal.bottom;
565
+ const endX = vLocal.left + vLocal.width / 2;
566
+ const endY = vLocal.top + vLocal.height / 2;
567
+ state.rayLine.setAttribute("x1", startX);
568
+ state.rayLine.setAttribute("y1", startY);
569
+ state.rayLine.setAttribute("x2", endX);
570
+ state.rayLine.setAttribute("y2", endY);
571
+ state.rayLine.style.display = "block";
572
+ }
573
+ }
574
+
575
+ function render(data) {
576
+ const root = getAppRoot().querySelector(`#${rootId}`);
577
+ if (!root) return null;
578
+ root.innerHTML = "";
579
+
580
+ const card = document.createElement("div");
581
+ card.className = "rosa-card";
582
+
583
+ const legend = document.createElement("div");
584
+ legend.className = "rosa-legend";
585
+ legend.innerHTML = `
586
+ <span class="legend-item"><span class="legend-dot legend-suffix"></span>Current suffix (t)</span>
587
+ <span class="legend-item"><span class="legend-dot legend-window"></span>k window (kkk[j:j+w])</span>
588
+ <span class="legend-item"><span class="legend-dot legend-match"></span>Match (kkk[j:j+w]==t)</span>
589
+ <span class="legend-item"><span class="legend-dot legend-v"></span>Read v (vvv[j+w])</span>
590
+ <span class="legend-item"><span class="legend-dot legend-out"></span>Output (out)</span>
591
+ `;
592
+ card.appendChild(legend);
593
+
594
+ const rowsWrap = document.createElement("div");
595
+ rowsWrap.className = "rosa-rows";
596
+
597
+ const indexRow = document.createElement("div");
598
+ indexRow.className = "rosa-row index-row";
599
+ const indexLabel = document.createElement("div");
600
+ indexLabel.className = "row-label";
601
+ indexLabel.textContent = "#";
602
+ const indexCells = document.createElement("div");
603
+ indexCells.className = "row-cells index-cells";
604
+ data.q.forEach((_, idx) => {
605
+ const cell = document.createElement("div");
606
+ cell.className = "index-cell";
607
+ cell.textContent = String(idx);
608
+ indexCells.appendChild(cell);
609
+ });
610
+ indexRow.appendChild(indexLabel);
611
+ indexRow.appendChild(indexCells);
612
+ rowsWrap.appendChild(indexRow);
613
+
614
+ const qRow = buildRow("q", data.q, "q-row");
615
+ const kRow = buildRow("k", data.k, "k-row");
616
+ const vRow = buildRow("v", data.v, "v-row");
617
+ const outRow = buildRow("out", data.q.map(() => "."), "out-row");
618
+
619
+ rowsWrap.appendChild(qRow.row);
620
+ rowsWrap.appendChild(kRow.row);
621
+ rowsWrap.appendChild(vRow.row);
622
+ rowsWrap.appendChild(outRow.row);
623
+
624
+ card.appendChild(rowsWrap);
625
+ const overlay = document.createElement("div");
626
+ overlay.className = "rosa-overlay";
627
+ const overlayRay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
628
+ overlayRay.classList.add("overlay-ray");
629
+ const rayDefs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
630
+ const rayMarker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
631
+ rayMarker.setAttribute("id", "rosa-ray-head");
632
+ rayMarker.setAttribute("markerWidth", "8");
633
+ rayMarker.setAttribute("markerHeight", "8");
634
+ rayMarker.setAttribute("refX", "6");
635
+ rayMarker.setAttribute("refY", "3");
636
+ rayMarker.setAttribute("orient", "auto");
637
+ const rayMarkerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
638
+ rayMarkerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
639
+ rayMarkerPath.setAttribute("fill", "var(--rosa-cyan)");
640
+ rayMarker.appendChild(rayMarkerPath);
641
+ rayDefs.appendChild(rayMarker);
642
+ overlayRay.appendChild(rayDefs);
643
+ const rayLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
644
+ rayLine.classList.add("overlay-ray-line");
645
+ rayLine.setAttribute("marker-end", "url(#rosa-ray-head)");
646
+ rayLine.style.display = "none";
647
+ overlayRay.appendChild(rayLine);
648
+ const overlayBoxes = document.createElement("div");
649
+ overlayBoxes.className = "overlay-box-layer";
650
+ overlay.appendChild(overlayBoxes);
651
+ overlay.appendChild(overlayRay);
652
+ card.appendChild(overlay);
653
+ const overlayHover = document.createElement("div");
654
+ overlayHover.className = "overlay-hover-layer";
655
+ card.appendChild(overlayHover);
656
+ root.appendChild(card);
657
+
658
+ const codeLines = getCodeLines();
659
+ resetCodeHighlight(codeLines);
660
+
661
+ return {
662
+ shellEl: root.closest("#rosa-shell") || getAppRoot().querySelector("#rosa-shell"),
663
+ cardEl: card,
664
+ qCells: qRow.cells,
665
+ kCells: kRow.cells,
666
+ vCells: vRow.cells,
667
+ outCells: outRow.cells,
668
+ outFixed: new Set(),
669
+ outPending: new Set(),
670
+ overlayBoxes,
671
+ rayLine,
672
+ overlayHover,
673
+ hoverBoxes: [],
674
+ lastOverlay: null,
675
+ codeLines,
676
+ activeLine: null,
677
+ };
678
+ }
679
+
680
+ function clearHighlights(state) {
681
+ const holdActive =
682
+ state.vHold &&
683
+ Number.isInteger(state.vHold.index) &&
684
+ performance.now() < state.vHold.until;
685
+ const holdIndex = holdActive ? state.vHold.index : null;
686
+ const all = [...state.qCells, ...state.kCells, ...state.vCells, ...state.outCells];
687
+ all.forEach((cell) => {
688
+ cell.classList.remove("active", "suffix", "k-window", "v-pick", "out");
689
+ });
690
+ if (holdActive && state.vCells[holdIndex]) {
691
+ state.vCells[holdIndex].classList.add("v-pick");
692
+ }
693
+ }
694
+
695
+ function applyStep(state, step) {
696
+ clearHighlights(state);
697
+ const { qCells, kCells, vCells, outCells } = state;
698
+ if (Number.isInteger(step.line)) {
699
+ setActiveCodeLine(state, step.line);
700
+ }
701
+
702
+ const showWindow = [
703
+ "loop_w",
704
+ "assign_t",
705
+ "loop_j",
706
+ "try",
707
+ "assign",
708
+ "break_inner",
709
+ "check",
710
+ "break_outer",
711
+ ].includes(step.phase);
712
+ if (showWindow && Number.isInteger(step.i) && qCells[step.i]) {
713
+ qCells[step.i].classList.add("active");
714
+ }
715
+ if (showWindow && Number.isInteger(step.w)) {
716
+ for (let idx = step.i - step.w + 1; idx <= step.i; idx += 1) {
717
+ if (qCells[idx]) qCells[idx].classList.add("suffix");
718
+ }
719
+ }
720
+ if (showWindow && Number.isInteger(step.j)) {
721
+ for (let idx = step.j; idx <= step.j + step.w - 1; idx += 1) {
722
+ if (kCells[idx]) kCells[idx].classList.add("k-window");
723
+ }
724
+ }
725
+ updateOverlay(state, step);
726
+
727
+ if (step.phase === "try") {
728
+ return;
729
+ }
730
+
731
+ if (step.phase === "assign") {
732
+ if (debugAnim) {
733
+ console.log("[ROSA] assign step", {
734
+ i: step.i,
735
+ j: step.j,
736
+ w: step.w,
737
+ v_index: step.v_index,
738
+ value: step.value,
739
+ });
740
+ }
741
+ if (Number.isInteger(step.v_index) && vCells[step.v_index]) {
742
+ vCells[step.v_index].classList.add("v-pick");
743
+ }
744
+ if (
745
+ Number.isInteger(step.v_index) &&
746
+ Number.isInteger(step.i) &&
747
+ vCells[step.v_index] &&
748
+ outCells[step.i]
749
+ ) {
750
+ const speed = getSpeed();
751
+ const holdToken = state.runToken;
752
+ if (state.outPending) {
753
+ state.outPending.add(step.i);
754
+ }
755
+ const pauseMs = transferPauseMs / speed;
756
+ const durationMs = transferMs / speed;
757
+ const totalWait = pauseMs + durationMs;
758
+ if (state.outPending) {
759
+ state.outPending.add(step.i);
760
+ }
761
+ const startTransfer = () => {
762
+ if (state.runToken !== holdToken) return;
763
+ animateTransfer(vCells[step.v_index], outCells[step.i], step.value, durationMs, () => {
764
+ if (state.runToken !== holdToken) return;
765
+ if (state.outPending) {
766
+ state.outPending.delete(step.i);
767
+ }
768
+ if (state.outFixed) {
769
+ state.outFixed.add(step.i);
770
+ }
771
+ const outCell = outCells[step.i];
772
+ if (outCell) {
773
+ outCell.textContent = String(step.value);
774
+ outCell.classList.add("out-fixed", "filled");
775
+ }
776
+ });
777
+ };
778
+ if (pauseMs > 0) {
779
+ setTimeout(startTransfer, pauseMs);
780
+ } else {
781
+ startTransfer();
782
+ }
783
+ state.vHold = {
784
+ index: step.v_index,
785
+ until: performance.now() + totalWait,
786
+ };
787
+ const holdIndex = step.v_index;
788
+ setTimeout(() => {
789
+ if (state.runToken !== holdToken) return;
790
+ if (!state.vHold || state.vHold.index !== holdIndex) return;
791
+ if (performance.now() < state.vHold.until) return;
792
+ const cell = state.vCells[holdIndex];
793
+ if (cell) cell.classList.remove("v-pick");
794
+ }, totalWait + 60);
795
+ return totalWait;
796
+ }
797
+ return 0;
798
+ }
799
+
800
+ if (step.phase === "break_inner") {
801
+ return;
802
+ }
803
+
804
+ if (step.phase === "check") {
805
+ return;
806
+ }
807
+
808
+ if (step.phase === "break_outer") {
809
+ return;
810
+ }
811
+
812
+ if (step.phase === "output") {
813
+ if (
814
+ (state.outPending && state.outPending.has(step.i)) ||
815
+ (state.outFixed && state.outFixed.has(step.i))
816
+ ) {
817
+ return;
818
+ }
819
+ if (outCells[step.i]) {
820
+ outCells[step.i].textContent = String(step.value);
821
+ outCells[step.i].classList.add("out", "out-fixed", "filled");
822
+ if (state.outFixed) {
823
+ state.outFixed.add(step.i);
824
+ }
825
+ }
826
+ return;
827
+ }
828
+
829
+ if (step.phase === "return") {
830
+ return;
831
+ }
832
+ }
833
+
834
+ async function play(state, steps, token) {
835
+ if (!steps || !steps.length) return;
836
+ for (let idx = 0; idx < steps.length; idx += 1) {
837
+ if (token !== runToken) return;
838
+ const extraWait = applyStep(state, steps[idx]);
839
+ const delay = baseDelay / getSpeed();
840
+ const waitMs = Math.max(delay, Number.isFinite(extraWait) ? extraWait : 0);
841
+ await sleep(waitMs);
842
+ }
843
+ }
844
+
845
+ function start(data) {
846
+ runToken += 1;
847
+ const token = runToken;
848
+ const state = render(data);
849
+ if (!state) return;
850
+ state.runToken = token;
851
+ state.vHold = null;
852
+ if (state.outFixed) state.outFixed.clear();
853
+ if (state.outPending) state.outPending.clear();
854
+ play(state, data.steps, token);
855
+ }
856
+
857
+ let lastValue = "";
858
+ safeInitThemeToggle();
859
+ setInterval(safeInitThemeToggle, 1200);
860
+ setInterval(() => {
861
+ const box = getStepsBox();
862
+ if (!box) return;
863
+ const value = box.value || "";
864
+ if (value && value !== lastValue) {
865
+ lastValue = value;
866
+ try {
867
+ const data = JSON.parse(value);
868
+ start(data);
869
+ } catch (err) {
870
+ console.error("Invalid ROSA steps payload", err);
871
+ }
872
+ }
873
+ }, 300);
874
+ }
875
+ """
876
+
877
+
878
+ def get_js_boot(js_func: str) -> str:
879
+ """获取 JavaScript 启动代码"""
880
+ return f"(function(){{ const init = {js_func}; init(); return init; }})()"
ui_handlers.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI 事件处理器 - 处理用户交互和输入验证
3
+ """
4
+
5
+ import random
6
+ import re
7
+ import gradio as gr
8
+ from algorithm import build_payload
9
+
10
+
11
+ MAX_DEMO_LEN = 20
12
+
13
+
14
+ def parse_bits(text: str, name: str) -> list[int]:
15
+ """
16
+ 解析二进制字符串输入
17
+ 允许 0/1,可以包含逗号和空格
18
+ """
19
+ cleaned = re.sub(r"[,\s]+", "", (text or "").strip())
20
+ if not cleaned:
21
+ raise gr.Error(f"{name} cannot be empty")
22
+ if re.search(r"[^01]", cleaned):
23
+ raise gr.Error(f"{name} must contain only 0/1 (spaces or commas allowed)")
24
+ return [int(c) for c in cleaned]
25
+
26
+
27
+ def on_demo(
28
+ q_text: str, k_text: str, v_text: str, line_numbers: dict
29
+ ) -> tuple[str, str]:
30
+ """
31
+ 处理演示按钮点击事件
32
+ 验证输入并生成步骤数据
33
+ """
34
+ q = parse_bits(q_text, "q")
35
+ k = parse_bits(k_text, "k")
36
+ v = parse_bits(v_text, "v")
37
+
38
+ if not (len(q) == len(k) == len(v)):
39
+ raise gr.Error("q, k, v must have the same length")
40
+ if len(q) > MAX_DEMO_LEN:
41
+ raise gr.Error(f"For smooth playback, length should be <= {MAX_DEMO_LEN}")
42
+
43
+ return build_payload(q, k, v, line_numbers)
44
+
45
+
46
+ def on_random(length: int) -> tuple[str, str, str]:
47
+ """
48
+ 生成随机的 q, k, v 序列
49
+ 用于演示目的
50
+ """
51
+ length = max(1, int(length))
52
+ q = [random.randint(0, 1) for _ in range(length)]
53
+ k = [random.randint(0, 1) for _ in range(length)]
54
+ v = [random.randint(0, 1) for _ in range(length)]
55
+ return (
56
+ "".join(str(x) for x in q),
57
+ "".join(str(x) for x in k),
58
+ "".join(str(x) for x in v),
59
+ )
ui_styles.py ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI 样式模块 - CSS 样式定义
3
+ """
4
+
5
+ CSS = """
6
+ :root,
7
+ .gradio-container {
8
+ --rosa-bg: #f8fafc;
9
+ --rosa-surface: #ffffff;
10
+ --rosa-surface-2: #f1f5f9;
11
+ --rosa-border: #e2e8f0;
12
+ --rosa-border-soft: #cbd5e1;
13
+ --rosa-text: #0f172a;
14
+ --rosa-text-muted: #475569;
15
+ --rosa-text-muted-2: #94a3b8;
16
+ --rosa-code-active-bg: #dbeafe;
17
+ --rosa-code-active-text: #1d4ed8;
18
+ --rosa-code-token-bg: #fef3c7;
19
+ --rosa-code-token-border: #f59e0b;
20
+ --rosa-card-shadow: none;
21
+ --rosa-float-bg: #f59e0b;
22
+ --rosa-float-text: #1f2937;
23
+ --rosa-legend-text: #475569;
24
+
25
+ /* Code Highlight Colors (Light) */
26
+ --rosa-tok-keyword: #7c3aed;
27
+ --rosa-tok-builtin: #0ea5e9;
28
+ --rosa-tok-number: #f97316;
29
+ --rosa-tok-string: #10b981;
30
+ --rosa-tok-comment: #64748b;
31
+
32
+ --rosa-blue: #3b82f6;
33
+ --rosa-sky: #38bdf8;
34
+ --rosa-violet: #a855f7;
35
+ --rosa-amber: #f59e0b;
36
+ --rosa-cyan: #06b6d4;
37
+ --rosa-green: #22c55e;
38
+ --rosa-green-soft: rgba(34, 197, 94, 0.16);
39
+ --rosa-red: #ef4444;
40
+ --rosa-red-soft: rgba(239, 68, 68, 0.14);
41
+ --rosa-blue-soft: rgba(59, 130, 246, 0.08);
42
+ --rosa-violet-soft: rgba(168, 85, 247, 0.08);
43
+ }
44
+
45
+ .rosa-theme-dark,
46
+ :root.rosa-theme-dark {
47
+ --rosa-bg: #0b1220;
48
+ --rosa-surface: #0f172a;
49
+ --rosa-surface-2: #1e293b;
50
+ --rosa-border: #334155;
51
+ --rosa-border-soft: #475569;
52
+ --rosa-text: #f1f5f9;
53
+ --rosa-text-muted: #cbd5e1;
54
+ --rosa-text-muted-2: #94a3b8;
55
+ --rosa-code-active-bg: rgba(59, 130, 246, 0.2);
56
+ --rosa-code-active-text: #bfdbfe;
57
+ --rosa-code-token-bg: rgba(245, 158, 11, 0.2);
58
+ --rosa-code-token-border: #fbbf24;
59
+ --rosa-card-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
60
+ --rosa-float-bg: #fbbf24;
61
+ --rosa-float-text: #111827;
62
+ --rosa-legend-text: #cbd5e1;
63
+
64
+ /* Code Highlight Colors (Dark) */
65
+ --rosa-tok-keyword: #c4b5fd;
66
+ --rosa-tok-builtin: #7dd3fc;
67
+ --rosa-tok-number: #fdba74;
68
+ --rosa-tok-string: #6ee7b7;
69
+ --rosa-tok-comment: #94a3b8;
70
+
71
+ --rosa-blue: #60a5fa;
72
+ --rosa-sky: #7dd3fc;
73
+ --rosa-violet: #c084fc;
74
+ --rosa-amber: #fbbf24;
75
+ --rosa-cyan: #67e8f9;
76
+ --rosa-green: #4ade80;
77
+ --rosa-green-soft: rgba(74, 222, 128, 0.2);
78
+ --rosa-red: #f87171;
79
+ --rosa-red-soft: rgba(248, 113, 113, 0.2);
80
+ --rosa-blue-soft: rgba(96, 165, 250, 0.2);
81
+ --rosa-violet-soft: rgba(192, 132, 252, 0.2);
82
+ }
83
+
84
+ body,
85
+ .gradio-container {
86
+ background: var(--rosa-bg) !important;
87
+ color: var(--rosa-text) !important;
88
+ }
89
+ .rosa-theme-dark {
90
+ color-scheme: dark;
91
+ }
92
+
93
+ /* 覆盖 Gradio 内置 CSS 变量 */
94
+ :root,
95
+ .gradio-container,
96
+ .rosa-theme-dark,
97
+ .rosa-theme-dark .gradio-container {
98
+ --body-background-fill: var(--rosa-bg) !important;
99
+ --block-background-fill: var(--rosa-surface) !important;
100
+ --block-border-color: var(--rosa-border) !important;
101
+ --block-label-background-fill: var(--rosa-surface) !important;
102
+ --block-label-text-color: var(--rosa-text) !important;
103
+ --block-title-text-color: var(--rosa-text) !important;
104
+ --input-background-fill: var(--rosa-surface-2) !important;
105
+ --input-border-color: var(--rosa-border) !important;
106
+ --input-text-color: var(--rosa-text) !important;
107
+ --body-text-color: var(--rosa-text) !important;
108
+ --body-text-color-subdued: var(--rosa-text-muted) !important;
109
+ --background-fill-primary: var(--rosa-surface) !important;
110
+ --background-fill-secondary: var(--rosa-surface-2) !important;
111
+ --border-color-primary: var(--rosa-border) !important;
112
+ --border-color-accent: var(--rosa-blue) !important;
113
+ --color-accent: var(--rosa-blue) !important;
114
+ --color-accent-soft: var(--rosa-blue-soft) !important;
115
+ --button-primary-background-fill: var(--rosa-blue) !important;
116
+ --button-primary-text-color: #f8fafc !important;
117
+ --button-secondary-background-fill: var(--rosa-surface-2) !important;
118
+ --button-secondary-text-color: var(--rosa-text) !important;
119
+ --button-secondary-border-color: var(--rosa-border) !important;
120
+ --neutral-50: var(--rosa-surface) !important;
121
+ --neutral-100: var(--rosa-surface-2) !important;
122
+ --neutral-200: var(--rosa-border) !important;
123
+ --neutral-300: var(--rosa-border-soft) !important;
124
+ --neutral-400: var(--rosa-text-muted-2) !important;
125
+ --neutral-500: var(--rosa-text-muted) !important;
126
+ --neutral-600: var(--rosa-text-muted) !important;
127
+ --neutral-700: var(--rosa-text) !important;
128
+ --neutral-800: var(--rosa-text) !important;
129
+ --neutral-900: var(--rosa-text) !important;
130
+ --neutral-950: var(--rosa-text) !important;
131
+ --panel-background-fill: var(--rosa-surface) !important;
132
+ --checkbox-background-color: var(--rosa-surface-2) !important;
133
+ --checkbox-border-color: var(--rosa-border) !important;
134
+ --slider-color: var(--rosa-blue) !important;
135
+ }
136
+
137
+ /* Gradio 按钮 */
138
+ .gradio-container button:not(.secondary),
139
+ .gradio-container .primary,
140
+ .gradio-container button[variant="primary"] {
141
+ background: var(--rosa-blue) !important;
142
+ color: #f8fafc !important;
143
+ border: 1px solid var(--rosa-blue) !important;
144
+ }
145
+ .gradio-container button.secondary,
146
+ .gradio-container button:not([variant="primary"]):not(.primary) {
147
+ background: var(--rosa-surface-2) !important;
148
+ color: var(--rosa-text) !important;
149
+ border: 1px solid var(--rosa-border) !important;
150
+ }
151
+
152
+ /* Gradio 输入框和文本区域 */
153
+ .gradio-container input[type="text"],
154
+ .gradio-container input[type="number"],
155
+ .gradio-container textarea {
156
+ background: var(--rosa-surface-2) !important;
157
+ color: var(--rosa-text) !important;
158
+ border: 1px solid var(--rosa-border) !important;
159
+ border-radius: 8px !important;
160
+ font-family: inherit;
161
+ }
162
+ .gradio-container input[readonly],
163
+ .gradio-container textarea[readonly],
164
+ .gradio-container textarea[disabled] {
165
+ background: var(--rosa-surface-2) !important;
166
+ color: var(--rosa-text) !important;
167
+ opacity: 1 !important;
168
+ -webkit-text-fill-color: var(--rosa-text) !important;
169
+ cursor: text !important;
170
+ }
171
+ .gradio-container ::placeholder,
172
+ .gradio-container input::placeholder,
173
+ .gradio-container textarea::placeholder {
174
+ color: var(--rosa-text-muted) !important;
175
+ opacity: 0.8;
176
+ }
177
+ .gradio-container input[type="text"]:focus,
178
+ .gradio-container input[type="number"]:focus,
179
+ .gradio-container textarea:focus {
180
+ outline: none !important;
181
+ border-color: var(--rosa-blue) !important;
182
+ box-shadow: 0 0 0 2px var(--rosa-blue-soft) !important;
183
+ }
184
+
185
+ /* Gradio 滑块 */
186
+ .gradio-container input[type="range"] {
187
+ accent-color: var(--rosa-blue);
188
+ }
189
+ .gradio-container .gr-box,
190
+ .gradio-container .gr-panel,
191
+ .gradio-container .gr-form {
192
+ background: var(--rosa-surface) !important;
193
+ border-color: var(--rosa-border) !important;
194
+ }
195
+
196
+ /* Gradio 标签 */
197
+ .gradio-container label,
198
+ .gradio-container .label,
199
+ .gradio-container span.label {
200
+ color: var(--rosa-text) !important;
201
+ }
202
+
203
+ /* Gradio 输出框 */
204
+ .gradio-container .output-class,
205
+ .gradio-container .gr-text-output {
206
+ background: var(--rosa-surface-2) !important;
207
+ color: var(--rosa-text) !important;
208
+ border: 1px solid var(--rosa-border) !important;
209
+ }
210
+
211
+ /* Gradio checkbox */
212
+ .gradio-container input[type="checkbox"] {
213
+ accent-color: var(--rosa-blue);
214
+ }
215
+
216
+ .page-header {
217
+ text-align: center;
218
+ margin-bottom: 18px;
219
+ color: var(--rosa-text);
220
+ }
221
+ .rosa-hidden {
222
+ display: none !important;
223
+ }
224
+ .page-title {
225
+ font-size: 30px;
226
+ font-weight: 700;
227
+ letter-spacing: 0.4px;
228
+ margin-bottom: 6px;
229
+ }
230
+ .page-subtitle {
231
+ font-size: 14px;
232
+ color: var(--rosa-text-muted);
233
+ }
234
+ .rosa-shell {
235
+ display: flex;
236
+ gap: 24px;
237
+ align-items: flex-start;
238
+ justify-content: center;
239
+ flex-wrap: wrap;
240
+ position: relative;
241
+ }
242
+ .rosa-pane {
243
+ flex: 3 1 520px;
244
+ min-width: 320px;
245
+ }
246
+ .rosa-code-pane {
247
+ flex: 2 1 320px;
248
+ min-width: 300px;
249
+ }
250
+ .quick-code-details {
251
+ margin-top: 8px;
252
+ }
253
+ .quick-code-details > summary {
254
+ cursor: pointer;
255
+ user-select: none;
256
+ padding: 8px 10px;
257
+ border: 1px dashed var(--rosa-border-soft);
258
+ border-radius: 12px;
259
+ background: var(--rosa-surface);
260
+ color: var(--rosa-text);
261
+ font-weight: 600;
262
+ }
263
+ .quick-code-details[open] > summary {
264
+ margin-bottom: 10px;
265
+ }
266
+ .quick-code-details > summary::-webkit-details-marker {
267
+ display: none;
268
+ }
269
+ .quick-code-details .rosa-code {
270
+ max-height: 420px;
271
+ }
272
+ #rosa-vis .rosa-card {
273
+ background: var(--rosa-surface);
274
+ border-radius: 18px;
275
+ padding: 24px;
276
+ color: var(--rosa-text);
277
+ box-shadow: var(--rosa-card-shadow);
278
+ border: 1px solid var(--rosa-border);
279
+ text-align: center;
280
+ position: relative;
281
+ }
282
+ #rosa-vis .rosa-rows {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 20px;
286
+ align-items: center;
287
+ }
288
+ #rosa-vis .rosa-row {
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 14px;
292
+ flex-wrap: wrap;
293
+ justify-content: center;
294
+ width: 100%;
295
+ }
296
+ #rosa-vis .rosa-row.k-row {
297
+ margin-bottom: 0;
298
+ }
299
+ #rosa-vis .row-label {
300
+ min-width: 36px;
301
+ font-size: 12px;
302
+ color: var(--rosa-text-muted);
303
+ text-transform: uppercase;
304
+ letter-spacing: 0.2px;
305
+ text-align: center;
306
+ }
307
+ #rosa-vis .row-cells {
308
+ display: flex;
309
+ flex-wrap: wrap;
310
+ gap: 6px;
311
+ justify-content: center;
312
+ max-width: 100%;
313
+ min-width: 0;
314
+ }
315
+ #rosa-vis .cell {
316
+ width: 30px;
317
+ height: 30px;
318
+ border-radius: 8px;
319
+ background: var(--rosa-surface-2);
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ font-weight: 600;
324
+ transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, color 0.18s ease;
325
+ color: var(--rosa-text);
326
+ box-shadow: none;
327
+ }
328
+ #rosa-vis .cell.active {
329
+ background: var(--rosa-blue);
330
+ color: #f8fafc;
331
+ box-shadow: none;
332
+ }
333
+ #rosa-vis .cell.suffix {
334
+ background: var(--rosa-sky);
335
+ color: #0b1220;
336
+ }
337
+ #rosa-vis .cell.k-window {
338
+ background: var(--rosa-violet);
339
+ color: #0b1220;
340
+ }
341
+ #rosa-vis .cell.v-pick {
342
+ background: var(--rosa-amber);
343
+ color: #1f2937;
344
+ }
345
+ #rosa-vis .cell.out,
346
+ #rosa-vis .cell.out-fixed {
347
+ background: var(--rosa-amber);
348
+ color: #1f2937;
349
+ }
350
+ #rosa-vis .cell.filled {
351
+ box-shadow: none;
352
+ }
353
+ #rosa-vis .rosa-overlay {
354
+ position: absolute;
355
+ inset: 0;
356
+ pointer-events: none;
357
+ z-index: 5;
358
+ }
359
+ #rosa-vis .overlay-svg {
360
+ position: absolute;
361
+ inset: 0;
362
+ width: 100%;
363
+ height: 100%;
364
+ pointer-events: none;
365
+ }
366
+ #rosa-vis .overlay-box-layer {
367
+ position: absolute;
368
+ inset: 0;
369
+ pointer-events: none;
370
+ }
371
+ #rosa-vis .overlay-box {
372
+ position: absolute;
373
+ border: 2px solid var(--rosa-blue);
374
+ border-radius: 10px;
375
+ box-shadow: 0 6px 16px rgba(15, 23, 42, 0.16);
376
+ box-sizing: border-box;
377
+ background: transparent;
378
+ transition: border-color 0.18s ease, background 0.18s ease;
379
+ }
380
+ #rosa-vis .overlay-hover-layer {
381
+ position: absolute;
382
+ inset: 0;
383
+ pointer-events: auto;
384
+ z-index: 6;
385
+ }
386
+ #rosa-vis .overlay-hover-box {
387
+ position: absolute;
388
+ background: transparent;
389
+ pointer-events: auto;
390
+ cursor: pointer;
391
+ }
392
+ #rosa-vis .overlay-box[data-label="t"] {
393
+ --overlay-label: "t";
394
+ }
395
+ #rosa-vis .overlay-box[data-label="kkk[j:j+w]"] {
396
+ --overlay-label: "kkk[j:j+w]";
397
+ }
398
+ #rosa-vis .overlay-box.t-box {
399
+ border-color: var(--rosa-blue);
400
+ background: var(--rosa-blue-soft);
401
+ }
402
+ #rosa-vis .overlay-box.k-box {
403
+ border-color: var(--rosa-violet);
404
+ background: var(--rosa-violet-soft);
405
+ }
406
+ #rosa-vis .overlay-box.try-match {
407
+ border-color: var(--rosa-green);
408
+ background: var(--rosa-green-soft);
409
+ }
410
+ #rosa-vis .overlay-box.try-miss {
411
+ border-color: var(--rosa-red);
412
+ background: var(--rosa-red-soft);
413
+ }
414
+ #rosa-vis .overlay-ray {
415
+ position: absolute;
416
+ inset: 0;
417
+ width: 100%;
418
+ height: 100%;
419
+ pointer-events: none;
420
+ }
421
+ #rosa-vis .overlay-ray-line {
422
+ stroke: var(--rosa-cyan);
423
+ stroke-width: 2;
424
+ fill: none;
425
+ stroke-linecap: round;
426
+ }
427
+ #rosa-vis .overlay-label {
428
+ position: absolute;
429
+ top: -12px;
430
+ right: 0;
431
+ font-size: 11px;
432
+ padding: 0;
433
+ background: transparent;
434
+ color: var(--rosa-text);
435
+ letter-spacing: 0.2px;
436
+ white-space: nowrap;
437
+ display: none;
438
+ }
439
+ #rosa-vis .overlay-label.t-label {
440
+ color: var(--rosa-blue);
441
+ }
442
+ #rosa-vis .overlay-label.k-label {
443
+ color: var(--rosa-violet);
444
+ }
445
+ .v-float {
446
+ position: fixed;
447
+ z-index: 9999;
448
+ border-radius: 8px;
449
+ background: var(--rosa-float-bg);
450
+ color: var(--rosa-float-text);
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ font-weight: 600;
455
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
456
+ pointer-events: none;
457
+ transition: transform 0.35s ease, opacity 0.35s ease;
458
+ opacity: 1;
459
+ }
460
+ #rosa-vis .rosa-legend {
461
+ display: flex;
462
+ flex-wrap: wrap;
463
+ gap: 12px;
464
+ margin-bottom: 12px;
465
+ font-size: 12px;
466
+ color: var(--rosa-legend-text);
467
+ justify-content: center;
468
+ }
469
+ #rosa-vis .legend-item {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ gap: 6px;
473
+ }
474
+ #rosa-vis .legend-dot {
475
+ width: 12px;
476
+ height: 12px;
477
+ border-radius: 4px;
478
+ }
479
+ #rosa-vis .legend-suffix {
480
+ background: var(--rosa-sky);
481
+ }
482
+ #rosa-vis .legend-window {
483
+ background: var(--rosa-violet);
484
+ }
485
+ #rosa-vis .legend-match {
486
+ background: var(--rosa-green);
487
+ }
488
+ #rosa-vis .legend-v {
489
+ background: var(--rosa-amber);
490
+ }
491
+ #rosa-vis .legend-out {
492
+ background: var(--rosa-amber);
493
+ }
494
+ #rosa-vis .index-row {
495
+ padding-bottom: 8px;
496
+ border-bottom: 1px dashed var(--rosa-border);
497
+ }
498
+ #rosa-vis .index-cells {
499
+ gap: 6px;
500
+ }
501
+ #rosa-vis .index-cell {
502
+ width: 30px;
503
+ text-align: center;
504
+ font-size: 11px;
505
+ color: var(--rosa-text-muted-2);
506
+ font-variant-numeric: tabular-nums;
507
+ }
508
+ .rosa-code {
509
+ background: var(--rosa-surface);
510
+ border: 1px solid var(--rosa-border);
511
+ border-radius: 12px;
512
+ padding: 12px;
513
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
514
+ font-size: 12px;
515
+ color: var(--rosa-text);
516
+ max-height: 520px;
517
+ overflow: auto;
518
+ }
519
+ .rosa-code .code-line {
520
+ display: flex;
521
+ gap: 10px;
522
+ padding: 2px 6px;
523
+ border-radius: 6px;
524
+ }
525
+ .rosa-code .line-no {
526
+ flex: 0 0 36px;
527
+ width: 36px;
528
+ text-align: left;
529
+ color: var(--rosa-text-muted-2);
530
+ user-select: none;
531
+ font-variant-numeric: tabular-nums;
532
+ }
533
+ .rosa-code .line-text {
534
+ white-space: pre;
535
+ flex: 1;
536
+ }
537
+ .rosa-code .code-line.active {
538
+ background: var(--rosa-code-active-bg);
539
+ color: var(--rosa-code-active-text);
540
+ }
541
+ .rosa-code .code-line.active .line-no {
542
+ color: var(--rosa-code-active-text);
543
+ }
544
+ .rosa-code .tok-keyword {
545
+ color: var(--rosa-tok-keyword);
546
+ font-weight: 600;
547
+ }
548
+ .rosa-code .tok-builtin {
549
+ color: var(--rosa-tok-builtin);
550
+ }
551
+ .rosa-code .tok-number {
552
+ color: var(--rosa-tok-number);
553
+ }
554
+ .rosa-code .tok-string {
555
+ color: var(--rosa-tok-string);
556
+ }
557
+ .rosa-code .tok-comment {
558
+ color: var(--rosa-tok-comment);
559
+ font-style: italic;
560
+ }
561
+ .rosa-code .code-token {
562
+ border-bottom: 1px dashed var(--rosa-text-muted-2);
563
+ padding: 0 1px;
564
+ }
565
+ .rosa-code .code-token.active {
566
+ background: var(--rosa-code-token-bg);
567
+ border-radius: 4px;
568
+ border-bottom-color: var(--rosa-code-token-border);
569
+ }
570
+ """