pal-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pal/__init__.py +29 -0
- pal/__main__.py +3 -0
- pal/access_manager/__init__.py +16 -0
- pal/access_manager/components/__init__.py +14 -0
- pal/access_manager/components/access_control.py +41 -0
- pal/access_manager/components/base.py +39 -0
- pal/access_manager/components/intervention.py +31 -0
- pal/access_manager/components/privilege_registry.py +30 -0
- pal/access_manager/pipeline.py +68 -0
- pal/access_manager/utils/__init__.py +2 -0
- pal/agent_constraints/__init__.py +3 -0
- pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
- pal/agent_constraints/components/__init__.py +3 -0
- pal/agent_constraints/components/constraints.py +22 -0
- pal/agent_constraints/pipeline.py +22 -0
- pal/agent_constraints/utils/__init__.py +2 -0
- pal/agents/__init__.py +12 -0
- pal/agents/coding.py +14 -0
- pal/agents/docs.py +15 -0
- pal/chat/__init__.py +4 -0
- pal/chat/demo.py +231 -0
- pal/chat/providers.py +23 -0
- pal/chat/service.py +591 -0
- pal/chat/services/__init__.py +26 -0
- pal/chat/services/base.py +142 -0
- pal/chat/services/data.py +142 -0
- pal/chat/services/media.py +123 -0
- pal/chat/services/web.py +60 -0
- pal/chat/sessions.py +108 -0
- pal/chat/tools.py +446 -0
- pal/chat/uploads.py +81 -0
- pal/cli.py +502 -0
- pal/compression/__init__.py +12 -0
- pal/compression/components/__init__.py +64 -0
- pal/compression/components/adverbs.py +20 -0
- pal/compression/components/hedges.py +54 -0
- pal/compression/components/interjections.py +20 -0
- pal/compression/components/minifier.py +56 -0
- pal/compression/components/output_compressor.py +13 -0
- pal/compression/components/pleonasms.py +54 -0
- pal/compression/components/punctuations.py +36 -0
- pal/compression/components/stopwords.py +62 -0
- pal/compression/history.py +346 -0
- pal/compression/pipeline.py +58 -0
- pal/compression/utils/__init__.py +30 -0
- pal/compression/utils/negations.py +18 -0
- pal/compression/utils/nltk_setup.py +20 -0
- pal/compression/utils/pronouns.py +27 -0
- pal/compression/utils/proselint_setup.py +9 -0
- pal/compression/utils/spacy_setup.py +22 -0
- pal/config/default.yaml +163 -0
- pal/config.py +504 -0
- pal/context_manager/__init__.py +7 -0
- pal/context_manager/chat_context.py +117 -0
- pal/context_manager/components/__init__.py +5 -0
- pal/context_manager/components/assembler.py +75 -0
- pal/context_manager/components/context_cache.py +45 -0
- pal/context_manager/components/kv_bridge.py +16 -0
- pal/context_manager/components/pager.py +48 -0
- pal/context_manager/components/simple_context_manager.py +56 -0
- pal/context_manager/components/turns.py +88 -0
- pal/context_manager/pipeline.py +27 -0
- pal/context_manager/utils/__init__.py +18 -0
- pal/context_manager/utils/hashing.py +5 -0
- pal/context_manager/utils/relevance.py +59 -0
- pal/context_manager/utils/storage.py +37 -0
- pal/documents/__init__.py +4 -0
- pal/documents/components/__init__.py +0 -0
- pal/documents/components/chunker.py +79 -0
- pal/documents/components/index.py +155 -0
- pal/documents/utils/__init__.py +1 -0
- pal/errors.py +40 -0
- pal/input_converters/__init__.py +8 -0
- pal/input_converters/components/__init__.py +0 -0
- pal/input_converters/components/base.py +38 -0
- pal/input_converters/components/pandoc_converter.py +87 -0
- pal/input_converters/components/pdf_converter.py +46 -0
- pal/input_converters/components/pptx_converter.py +52 -0
- pal/input_converters/components/xlsx_converter.py +38 -0
- pal/input_converters/pipeline.py +72 -0
- pal/input_converters/utils/__init__.py +1 -0
- pal/input_converters/workspace.py +56 -0
- pal/input_savers/INPUT_RULES.md +2 -0
- pal/input_savers/__init__.py +5 -0
- pal/input_savers/components/__init__.py +0 -0
- pal/input_savers/components/base.py +40 -0
- pal/input_savers/components/methods.py +41 -0
- pal/input_savers/components/verifier.py +39 -0
- pal/input_savers/pipeline.py +154 -0
- pal/input_savers/utils/__init__.py +0 -0
- pal/input_savers/utils/guards.py +94 -0
- pal/knowledge_graph/__init__.py +10 -0
- pal/knowledge_graph/components/__init__.py +0 -0
- pal/knowledge_graph/components/base.py +50 -0
- pal/knowledge_graph/components/builder.py +54 -0
- pal/knowledge_graph/components/retriever.py +100 -0
- pal/knowledge_graph/components/workspace.py +116 -0
- pal/knowledge_graph/pipeline.py +215 -0
- pal/knowledge_graph/utils/__init__.py +1 -0
- pal/latency_tracker/__init__.py +3 -0
- pal/latency_tracker/pipeline.py +86 -0
- pal/llm_scheduler/__init__.py +11 -0
- pal/llm_scheduler/api.py +137 -0
- pal/llm_scheduler/code.py +28 -0
- pal/llm_scheduler/core.py +92 -0
- pal/llm_scheduler/local.py +215 -0
- pal/llm_scheduler/pipeline.py +42 -0
- pal/llm_scheduler/types.py +92 -0
- pal/llm_wrapper/__init__.py +34 -0
- pal/llm_wrapper/adapter.py +316 -0
- pal/llm_wrapper/components/__init__.py +23 -0
- pal/llm_wrapper/components/adapter_types.py +90 -0
- pal/llm_wrapper/components/base.py +195 -0
- pal/llm_wrapper/components/features.py +104 -0
- pal/llm_wrapper/components/litellm_wrapper.py +215 -0
- pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
- pal/llm_wrapper/modes.py +73 -0
- pal/llm_wrapper/pipeline.py +61 -0
- pal/llm_wrapper/utils/__init__.py +4 -0
- pal/llm_wrapper/utils/registry.py +49 -0
- pal/llm_wrapper/utils/token_counting.py +10 -0
- pal/local_llm/__init__.py +5 -0
- pal/local_llm/components/__init__.py +0 -0
- pal/local_llm/components/huggingface_local.py +217 -0
- pal/local_llm/components/local_llm.py +60 -0
- pal/logging_setup.py +18 -0
- pal/memory/__init__.py +7 -0
- pal/memory/components/__init__.py +6 -0
- pal/memory/components/lru_k.py +56 -0
- pal/memory/components/manager.py +98 -0
- pal/memory/components/note.py +54 -0
- pal/memory/components/storage_manager.py +60 -0
- pal/memory/pipeline.py +122 -0
- pal/memory/utils/__init__.py +3 -0
- pal/memory/utils/keyword_overlap.py +3 -0
- pal/memory_tracker/__init__.py +3 -0
- pal/memory_tracker/components/__init__.py +0 -0
- pal/memory_tracker/components/process.py +56 -0
- pal/memory_tracker/pipeline.py +44 -0
- pal/metrics/__init__.py +4 -0
- pal/metrics/components/__init__.py +4 -0
- pal/metrics/components/collector.py +140 -0
- pal/metrics/components/exporter.py +28 -0
- pal/metrics/pipeline.py +49 -0
- pal/metrics/utils/__init__.py +2 -0
- pal/notes_manager/__init__.py +5 -0
- pal/notes_manager/components/__init__.py +0 -0
- pal/notes_manager/components/privacy.py +14 -0
- pal/notes_manager/components/recall.py +31 -0
- pal/notes_manager/components/tidy.py +28 -0
- pal/notes_manager/pipeline.py +138 -0
- pal/orchestrator/__init__.py +26 -0
- pal/orchestrator/components/__init__.py +26 -0
- pal/orchestrator/components/agent.py +105 -0
- pal/orchestrator/components/cgroup_manager.py +209 -0
- pal/orchestrator/components/context_interrupt.py +220 -0
- pal/orchestrator/components/limits.py +9 -0
- pal/orchestrator/components/scheduler.py +69 -0
- pal/orchestrator/components/syscall_scheduler.py +200 -0
- pal/orchestrator/pipeline.py +228 -0
- pal/orchestrator/utils/__init__.py +4 -0
- pal/orchestrator/utils/device.py +164 -0
- pal/orchestrator/utils/protocol.py +32 -0
- pal/os_kernel/__init__.py +5 -0
- pal/os_kernel/filesystem.py +80 -0
- pal/os_kernel/network.py +146 -0
- pal/os_kernel/pipeline.py +175 -0
- pal/os_kernel/sysinfo.py +36 -0
- pal/paths.py +59 -0
- pal/pipeline.py +177 -0
- pal/py.typed +0 -0
- pal/redact.py +19 -0
- pal/sdk/__init__.py +33 -0
- pal/sdk/adapters.py +43 -0
- pal/sdk/agent.py +99 -0
- pal/sdk/components/__init__.py +14 -0
- pal/sdk/components/autogen_adapter.py +65 -0
- pal/sdk/components/autogen_client.py +98 -0
- pal/sdk/components/framework_adapter_base.py +69 -0
- pal/sdk/components/metagpt_adapter.py +46 -0
- pal/sdk/components/open_interpreter_adapter.py +47 -0
- pal/sdk/components/tool_catalog.py +44 -0
- pal/sdk/os_api.py +84 -0
- pal/sdk/patterns.py +129 -0
- pal/sdk/pipeline.py +240 -0
- pal/sdk/queries.py +68 -0
- pal/sdk/savers.py +88 -0
- pal/sdk/utils/__init__.py +2 -0
- pal/storage_manager/__init__.py +17 -0
- pal/storage_manager/components/__init__.py +19 -0
- pal/storage_manager/components/base.py +50 -0
- pal/storage_manager/components/local_file_backend.py +84 -0
- pal/storage_manager/components/storage_queue.py +91 -0
- pal/storage_manager/components/vector_store_backend.py +122 -0
- pal/storage_manager/pipeline.py +153 -0
- pal/storage_manager/utils/__init__.py +4 -0
- pal/storage_manager/utils/memory_adapter.py +60 -0
- pal/storage_manager/utils/sdk.py +93 -0
- pal/studio/__init__.py +0 -0
- pal/studio/builder.py +166 -0
- pal/studio/catalog.py +90 -0
- pal/studio/checks.py +129 -0
- pal/studio/clients.py +6 -0
- pal/studio/editor.py +137 -0
- pal/studio/files.py +6 -0
- pal/studio/models.py +6 -0
- pal/studio/plan.py +149 -0
- pal/studio/planner.py +205 -0
- pal/studio/policy.py +69 -0
- pal/studio/routes.py +254 -0
- pal/studio/runs.py +210 -0
- pal/task_router/__init__.py +11 -0
- pal/task_router/components/__init__.py +0 -0
- pal/task_router/components/answerer.py +37 -0
- pal/task_router/components/base.py +34 -0
- pal/task_router/components/classifier.py +40 -0
- pal/task_router/components/gates.py +23 -0
- pal/task_router/pipeline.py +61 -0
- pal/task_router/utils/__init__.py +1 -0
- pal/terminal/__init__.py +1 -0
- pal/terminal/backend.py +141 -0
- pal/terminal/bench.py +173 -0
- pal/terminal/cli.py +826 -0
- pal/terminal/clients.py +170 -0
- pal/terminal/commands.py +401 -0
- pal/terminal/decide.py +242 -0
- pal/terminal/files.py +55 -0
- pal/terminal/flow_cli.py +439 -0
- pal/terminal/hardware.py +199 -0
- pal/terminal/history.py +88 -0
- pal/terminal/keys.py +83 -0
- pal/terminal/local_models.py +261 -0
- pal/terminal/logo.py +117 -0
- pal/terminal/models.py +99 -0
- pal/terminal/policy.py +54 -0
- pal/terminal/project.py +413 -0
- pal/terminal/review.py +65 -0
- pal/terminal/security.py +239 -0
- pal/terminal/session.py +503 -0
- pal/terminal/setup.py +354 -0
- pal/terminal/syntax.py +98 -0
- pal/terminal/theme.py +37 -0
- pal/terminal/ui.py +647 -0
- pal/terminal/voice.py +137 -0
- pal/terminal/workflow.py +624 -0
- pal/token_tracker/__init__.py +3 -0
- pal/token_tracker/pipeline.py +54 -0
- pal/tokens.py +21 -0
- pal/tool_catalog.py +107 -0
- pal/tool_manager/__init__.py +17 -0
- pal/tool_manager/builtin.py +82 -0
- pal/tool_manager/components/__init__.py +19 -0
- pal/tool_manager/components/base.py +53 -0
- pal/tool_manager/components/conflict_resolver.py +27 -0
- pal/tool_manager/components/param_validator.py +41 -0
- pal/tool_manager/components/tool_call_queue.py +111 -0
- pal/tool_manager/components/tool_registry.py +60 -0
- pal/tool_manager/pipeline.py +80 -0
- pal/tool_manager/utils/__init__.py +3 -0
- pal/web/__init__.py +3 -0
- pal/web/app.py +276 -0
- pal/web/static/editor.js +219 -0
- pal/web/static/fonts/LICENSE-Inter.txt +3 -0
- pal/web/static/fonts/inter-400.woff2 +0 -0
- pal/web/static/fonts/inter-500.woff2 +0 -0
- pal/web/static/fonts/inter-600.woff2 +0 -0
- pal/web/static/fonts/inter-700.woff2 +0 -0
- pal/web/static/inter.css +5 -0
- pal/web/static/pal-logo-256.png +0 -0
- pal/web/static/pal-logo-32.png +0 -0
- pal/web/static/pal-logo-64.png +0 -0
- pal/web/static/studio.css +306 -0
- pal/web/static/studio.js +533 -0
- pal/web/static/voice.js +76 -0
- pal/web/templates/index.html +368 -0
- pal/web/templates/studio.html +120 -0
- pal_code-0.1.0.dist-info/METADATA +379 -0
- pal_code-0.1.0.dist-info/RECORD +282 -0
- pal_code-0.1.0.dist-info/WHEEL +5 -0
- pal_code-0.1.0.dist-info/entry_points.txt +2 -0
- pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- pal_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Pal</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/static/pal-logo-32.png">
|
|
8
|
+
<link rel="apple-touch-icon" href="/static/pal-logo-256.png">
|
|
9
|
+
<link rel="stylesheet" href="/static/inter.css">
|
|
10
|
+
<style>
|
|
11
|
+
:root{color-scheme:dark;--bg:#000;--surface:#141414;--ink:#fff;--muted:#a3a3a3;--line:#2e2e2e;--accent:#E50914;--kept:#FF4D57;--rev:#d9a441;--skip:#8a8a8a}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
html,body{height:100%;margin:0}
|
|
14
|
+
body{background:var(--bg);color:var(--ink);font:15px/1.5 "Inter",system-ui,-apple-system,"Segoe UI",sans-serif;display:flex;flex-direction:column}
|
|
15
|
+
header{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--surface);border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
16
|
+
header h1{font-size:16px;margin:0 8px 0 0;display:flex;align-items:center;gap:8px}
|
|
17
|
+
.logo{width:28px;height:28px;object-fit:contain}
|
|
18
|
+
.msg .who .av{width:18px;height:18px;object-fit:contain;vertical-align:-4px;margin-right:6px}
|
|
19
|
+
select,input,textarea,button{font:inherit;color:inherit}
|
|
20
|
+
select,input[type=text],input[type=password]{border:1px solid var(--line);background:var(--surface);border-radius:6px;padding:6px 8px}
|
|
21
|
+
button{border:1px solid var(--line);background:var(--surface);border-radius:6px;padding:6px 12px;cursor:pointer}
|
|
22
|
+
.linkstudio{border:1px solid var(--line);border-radius:6px;padding:6px 12px;text-decoration:none;color:var(--ink);background:var(--surface)}
|
|
23
|
+
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
24
|
+
button:disabled{opacity:.5;cursor:default}
|
|
25
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
26
|
+
.sp{flex:1}
|
|
27
|
+
details.pop{position:relative}
|
|
28
|
+
details.pop>summary{list-style:none;cursor:pointer;border:1px solid var(--line);border-radius:6px;padding:6px 12px;background:var(--surface)}
|
|
29
|
+
details.pop>summary::-webkit-details-marker{display:none}
|
|
30
|
+
details.pop .box{position:absolute;right:0;top:110%;z-index:5;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:12px;min-width:270px;box-shadow:0 6px 24px rgba(0,0,0,.7)}
|
|
31
|
+
.box label{display:flex;gap:8px;align-items:center;padding:3px 0}
|
|
32
|
+
.box .k{width:100%;margin-top:6px}
|
|
33
|
+
main{flex:1;display:flex;min-height:0}
|
|
34
|
+
#chatcol{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
35
|
+
#log{flex:1;overflow-y:auto;padding:20px 16px}
|
|
36
|
+
.wrapc{max-width:760px;margin:0 auto}
|
|
37
|
+
.msg{margin:0 0 18px}
|
|
38
|
+
.msg .who{font-size:12px;color:var(--muted);margin-bottom:2px}
|
|
39
|
+
.msg .body{white-space:pre-wrap;word-wrap:break-word}
|
|
40
|
+
.msg.user .body{background:rgba(229,9,20,.22);border-radius:10px;padding:8px 12px;display:inline-block;max-width:100%}
|
|
41
|
+
.meta{font-size:12px;color:var(--muted);margin-top:4px}
|
|
42
|
+
.meta summary{cursor:pointer}
|
|
43
|
+
pre{white-space:pre-wrap;word-wrap:break-word;background:#1a1a1a;border:1px solid var(--line);border-radius:6px;padding:8px;margin:4px 0 0;font:12px/1.4 ui-monospace,Consolas,monospace}
|
|
44
|
+
.err{color:#FF4D57}
|
|
45
|
+
#mic{border:1px solid var(--line);background:transparent;border-radius:10px;padding:0 10px;cursor:pointer}#mic.live{background:var(--red,#E50914);color:#fff}
|
|
46
|
+
#composer{border-top:1px solid var(--line);background:var(--surface);padding:10px 16px}
|
|
47
|
+
#chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
|
|
48
|
+
.chip{font-size:12px;background:#262626;border-radius:12px;padding:2px 4px 2px 10px}
|
|
49
|
+
.chip button{border:0;background:none;padding:0 6px;color:var(--muted)}
|
|
50
|
+
#row{display:flex;gap:8px;align-items:flex-end}
|
|
51
|
+
#msg{flex:1;resize:none;border:1px solid var(--line);border-radius:8px;padding:8px 10px;max-height:200px;background:var(--surface)}
|
|
52
|
+
#expin h2,#expview h2{font-size:17px;margin:0 0 6px}
|
|
53
|
+
#etext{width:100%;height:110px;border:1px solid var(--line);border-radius:8px;padding:8px;resize:vertical}
|
|
54
|
+
.opts{display:flex;flex-wrap:wrap;gap:6px 14px;margin:8px 0;align-items:center}
|
|
55
|
+
.opts label{display:flex;gap:6px;align-items:center}
|
|
56
|
+
.sum{margin:14px 0 6px;font-size:17px}
|
|
57
|
+
.sum b{color:var(--kept)}
|
|
58
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
59
|
+
th{text-align:left;font-weight:600;color:var(--muted);padding:4px 6px;border-bottom:1px solid var(--line)}
|
|
60
|
+
td{padding:6px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
61
|
+
tr.step{cursor:pointer}
|
|
62
|
+
.st{font-weight:600}.st.kept{color:var(--kept)}.st.reverted{color:var(--rev)}.st.skipped{color:var(--skip)}
|
|
63
|
+
.bar{height:6px;background:#2e2e2e;border-radius:3px;margin-top:4px}
|
|
64
|
+
.bar i{display:block;height:100%;background:var(--accent);border-radius:3px}
|
|
65
|
+
.cmp{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px}
|
|
66
|
+
.note{color:var(--muted);font-size:12px;margin-top:6px}
|
|
67
|
+
#tabs{display:flex;gap:2px;padding:0 16px;background:var(--surface);border-bottom:1px solid var(--line)}
|
|
68
|
+
.tab{border:0;border-bottom:3px solid transparent;border-radius:0;background:none;padding:10px 16px;font-weight:600;color:var(--muted)}
|
|
69
|
+
.tab.on{color:var(--accent);border-bottom-color:var(--accent)}
|
|
70
|
+
.view{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
71
|
+
.view[hidden]{display:none}
|
|
72
|
+
#savebar{display:flex;gap:6px 18px;flex-wrap:wrap;align-items:center;padding:6px 16px;background:#1a1a1a;border-bottom:1px solid #2e2e2e;font-size:13px;color:#FF4D57}
|
|
73
|
+
#savebar b{font-size:15px}
|
|
74
|
+
#savebar .dim{color:var(--muted)}
|
|
75
|
+
#expview{overflow-y:auto}
|
|
76
|
+
#expview .inner{max-width:920px;margin:0 auto;padding:20px 16px 40px;width:100%}
|
|
77
|
+
#dtable td.num,#dtable th.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
78
|
+
#dtable tr.run td{background:#f4f7fd}
|
|
79
|
+
.badge{display:inline-block;font-size:11px;border-radius:9px;padding:1px 8px;background:#262626}
|
|
80
|
+
.badge.local{background:#e3f4ea;color:#1f7a4d}.badge.tool{background:#fdf1dc;color:#8a5a00}
|
|
81
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin:16px 0}
|
|
82
|
+
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
|
|
83
|
+
.card .lab{font-size:12px;color:var(--muted)}
|
|
84
|
+
.card .big{font-size:30px;font-weight:700;line-height:1.15;color:var(--kept)}
|
|
85
|
+
.card .big.bad{color:var(--rev)}
|
|
86
|
+
.card .sub{font-size:13px;color:var(--muted);margin-top:4px}
|
|
87
|
+
.mt{margin-top:18px}
|
|
88
|
+
.result{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 16px;white-space:pre-wrap}
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<header>
|
|
93
|
+
<h1><img src="/static/pal-logo-64.png" alt="" class="logo" width="28" height="28">Pal</h1>
|
|
94
|
+
<select id="agent" aria-label="Agent" title="Agents use Gemini with their own key from your .env file. All token savers still apply."><option value="">General chat</option></select>
|
|
95
|
+
<select id="llmmode" aria-label="How to reach models" title="Explore: one OpenRouter key reaches hundreds of models, some free (your prompts pass through OpenRouter). Own key: your key for one provider, direct."></select>
|
|
96
|
+
<select id="prov" aria-label="Provider"></select>
|
|
97
|
+
<label id="freebox" hidden title="Only list free models" style="font-size:12px"><input type="checkbox" id="freeonly"> free only</label>
|
|
98
|
+
<input id="model" type="text" list="mlist" placeholder="model id" size="26" aria-label="Model">
|
|
99
|
+
<datalist id="mlist"></datalist>
|
|
100
|
+
<details class="pop"><summary id="msum">Input savers</summary>
|
|
101
|
+
<div class="box">
|
|
102
|
+
<label><input type="radio" name="mode" value="rules" checked> Rules file (one pass)</label>
|
|
103
|
+
<label><input type="radio" name="mode" value="steps"> Step methods</label>
|
|
104
|
+
<div id="mlistbox" hidden></div>
|
|
105
|
+
<div class="note">Pasted text, code, URLs and attached files are never edited. A zip of source code is indexed as a graph; big documents are indexed too; only relevant excerpts are sent.</div>
|
|
106
|
+
<div class="note" id="lstat">Local LLM: checking…</div></div></details>
|
|
107
|
+
<select id="route" aria-label="Routing" title="Who answers: easy tasks go to the local LLM in auto mode">
|
|
108
|
+
<option value="auto">Routing: auto</option><option value="local">Local LLM only</option><option value="api">API model only</option></select>
|
|
109
|
+
<details class="pop"><summary>API key</summary>
|
|
110
|
+
<div class="box"><div class="note">Kept in this browser tab only. Or set the provider's env var.</div>
|
|
111
|
+
<input id="key" type="password" class="k" placeholder="paste key" autocomplete="off"></div></details>
|
|
112
|
+
<details class="pop"><summary id="statsum">Stats</summary>
|
|
113
|
+
<div class="box"><pre id="statbox" style="white-space:pre-wrap;font-size:12px;margin:0">Loading…</pre>
|
|
114
|
+
<div class="note"><button id="statrefresh">Refresh</button> <button id="statreset">Reset</button></div></div></details>
|
|
115
|
+
<details class="pop"><summary id="memsum">Memory</summary>
|
|
116
|
+
<div class="box"><div class="note" id="memnote">Checking…</div>
|
|
117
|
+
<div class="note">Your messages are kept on this machine so later sessions can recall them. Secrets are skipped.</div>
|
|
118
|
+
<button id="memclear">Forget everything</button></div></details>
|
|
119
|
+
<span class="sp"></span>
|
|
120
|
+
<a href="/studio" class="linkstudio" title="Describe a product and let the bot plan and build it">Studio</a>
|
|
121
|
+
<button id="newchat">New chat</button>
|
|
122
|
+
</header>
|
|
123
|
+
<main>
|
|
124
|
+
<section id="chatcol">
|
|
125
|
+
<nav id="tabs" role="tablist"><button class="tab on" data-tab="chat" role="tab">Chat</button><button class="tab" data-tab="exp" role="tab">Experiment</button></nav>
|
|
126
|
+
<div id="chatview" class="view">
|
|
127
|
+
<div id="savebar" title="Estimates: characters / 4. Input savers, older turns left out, only relevant parts of documents and code, and turns answered without the API model."><span>Saved this chat: <b id="sv1">0</b> tokens (est.)</span><span><b id="sv2">0</b> API calls avoided</span><span class="dim" id="sv3">API used: 0 in / 0 out</span></div>
|
|
128
|
+
<div id="log"><div class="wrapc" id="msgs"></div></div>
|
|
129
|
+
<div id="composer"><div class="wrapc">
|
|
130
|
+
<div id="chips"></div>
|
|
131
|
+
<div id="row">
|
|
132
|
+
<button id="attach" title="Attach files" aria-label="Attach files">+</button>
|
|
133
|
+
<input type="file" id="file" multiple hidden>
|
|
134
|
+
<textarea id="msg" rows="1" placeholder="Message. Paste text or files. Enter sends, Shift+Enter adds a line."></textarea>
|
|
135
|
+
<button id="mic" title="Speak instead of typing" aria-label="Speak instead of typing" aria-pressed="false">🎙</button>
|
|
136
|
+
<button id="send" class="primary">Send</button>
|
|
137
|
+
</div></div></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="expview" class="view" hidden><div class="inner">
|
|
140
|
+
<h2>Demo: plan a launch in 6 messages</h2>
|
|
141
|
+
<p class="note" style="margin:0 0 10px">Sends the same 6 messages twice to the model chosen in the header: once the way a plain chat app does it (whole history every time, no savers), once through Pal. Then compares the tokens the API reports and the time taken. About 12 API calls. It uses no memory and is not added to Stats.</p>
|
|
142
|
+
<div class="opts"><button id="drun" class="primary">Run demo</button><span id="dstat" class="note"></span></div>
|
|
143
|
+
<table id="dtable" hidden><thead><tr><th>#</th><th>Message</th><th>Pal did</th><th class="num">Plain tokens<br>in / out</th><th class="num">Pal tokens<br>in / out</th><th class="num">Plain s</th><th class="num">Pal s</th></tr></thead><tbody id="dbody"></tbody></table>
|
|
144
|
+
<div id="dsum"></div>
|
|
145
|
+
<details class="mt"><summary>Test the input savers on your own text</summary>
|
|
146
|
+
<textarea id="etext" placeholder="Text to test the savers on"></textarea>
|
|
147
|
+
<div class="opts">
|
|
148
|
+
<label><input type="checkbox" id="ecall"> also call the API model, raw vs saved</label>
|
|
149
|
+
<button id="euse">Use chat box text</button>
|
|
150
|
+
</div>
|
|
151
|
+
<button id="erun" class="primary">Run experiment</button>
|
|
152
|
+
<div id="eout"></div>
|
|
153
|
+
</details>
|
|
154
|
+
</div></div>
|
|
155
|
+
</section>
|
|
156
|
+
</main>
|
|
157
|
+
<script src="/static/voice.js"></script>
|
|
158
|
+
<script>
|
|
159
|
+
const $=id=>document.getElementById(id);
|
|
160
|
+
let sid=(sessionStorage.sid||=crypto.randomUUID());
|
|
161
|
+
let files=[],methods=[],cfg=null,pasted=[],mode='rules';
|
|
162
|
+
const esc=s=>String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
|
|
163
|
+
const MODELS={anthropic:['claude-sonnet-5-5','claude-haiku-4-5-20251001','claude-opus-5-5'],gemini:['gemini-3.5-flash','gemini-2.5-flash','gemini-2.5-flash-lite']};
|
|
164
|
+
|
|
165
|
+
function sumMethods(){methods=[...document.querySelectorAll('.mchk:checked')].map(x=>x.value);
|
|
166
|
+
$('msum').textContent=mode==='rules'?'Input savers: rules file':`Input savers: ${methods.length}/${cfg.methods.length} steps`}
|
|
167
|
+
document.querySelectorAll('input[name=mode]').forEach(r=>r.onchange=()=>{mode=document.querySelector('input[name=mode]:checked').value;$('mlistbox').hidden=mode!=='steps';sumMethods()});
|
|
168
|
+
let PROVS={},LLM_MODES=[],CATALOGUE=[];
|
|
169
|
+
function applyLlmMode(){const key=$('llmmode').value,m=LLM_MODES.find(x=>x.key===key)||{providers:[]};sessionStorage.llmmode=key;
|
|
170
|
+
const names=m.providers.map(p=>p.name);
|
|
171
|
+
$('prov').innerHTML=names.map(p=>`<option value="${p}">${p}${PROVS[p]?' (key set)':''}</option>`).join('');
|
|
172
|
+
const keep=names.includes(sessionStorage.prov)?sessionStorage.prov:(names.find(p=>PROVS[p])||names[0]);$('prov').value=keep;sessionStorage.prov=keep;
|
|
173
|
+
$('llmmode').title=m.summary||'';$('freebox').hidden=key!=='explore';setModels();if(key==='explore')loadCatalogue()}
|
|
174
|
+
async function loadCatalogue(){try{const free=$('freeonly').checked?'&free=1':'';const d=await (await fetch('/api/llm/models?limit=500'+free)).json();
|
|
175
|
+
if(!d.models)throw new Error(d.error||'no models');CATALOGUE=d.models;$('model').placeholder='pick one of '+CATALOGUE.length+' models, or type a name';
|
|
176
|
+
$('mlist').innerHTML=CATALOGUE.map(m=>`<option value="${esc(m.id)}" label="${esc(m.label)}">`).join('')}
|
|
177
|
+
catch(e){CATALOGUE=[];$('model').placeholder='the model list could not be read: type a model name'}}
|
|
178
|
+
function setModels(){const p=$('prov').value;$('mlist').innerHTML=(MODELS[p]||[]).map(m=>`<option value="${m}">`).join('');$('model').value=sessionStorage['model_'+p]||(MODELS[p]||[''])[0];$('key').value=sessionStorage['key_'+p]||''}
|
|
179
|
+
|
|
180
|
+
fetch('/api/config').then(r=>r.json()).then(c=>{
|
|
181
|
+
$('agent').insertAdjacentHTML('beforeend',(c.agents||[]).map(a=>`<option value="${a.id}" ${a.ready?'':'disabled'} title="${esc(a.description)}">${esc(a.name)}${a.ready?'':' (set '+a.key_env+')'}</option>`).join(''));
|
|
182
|
+
$('agent').value=sessionStorage.agent||'';applyAgent();
|
|
183
|
+
cfg=c;mode=c.mode;if(c.routing){$('route').value=c.routing.default;$('route').disabled=!c.routing.enabled}document.querySelector(`input[name=mode][value=${mode}]`).checked=true;$('mlistbox').hidden=mode!=='steps';
|
|
184
|
+
PROVS=c.providers;LLM_MODES=c.llm_modes||[];
|
|
185
|
+
$('llmmode').innerHTML=LLM_MODES.map(m=>`<option value="${m.key}" title="${esc(m.summary)}">${esc(m.title)}</option>`).join('');
|
|
186
|
+
$('llmmode').value=sessionStorage.llmmode||c.llm_mode||'own_key';applyLlmMode();
|
|
187
|
+
$('mlistbox').innerHTML=c.methods.map(m=>`<label><input type="checkbox" class="mchk" value="${m.id}" checked> ${esc(m.name)}</label>`).join('');
|
|
188
|
+
document.querySelectorAll('.mchk').forEach(x=>x.onchange=sumMethods);
|
|
189
|
+
sumMethods();setModels();
|
|
190
|
+
});
|
|
191
|
+
fetch('/api/local_status').then(r=>r.json()).then(s=>{$('lstat').textContent='Local LLM: '+s.detail;if(s.ok===false)$('lstat').classList.add('err')});
|
|
192
|
+
$('prov').onchange=()=>{sessionStorage.prov=$('prov').value;setModels();if($('llmmode').value==='explore'&&CATALOGUE.length)loadCatalogue()};
|
|
193
|
+
$('llmmode').onchange=applyLlmMode;$('freeonly').onchange=loadCatalogue;
|
|
194
|
+
$('model').onchange=()=>sessionStorage['model_'+$('prov').value]=$('model').value;
|
|
195
|
+
$('key').oninput=()=>sessionStorage['key_'+$('prov').value]=$('key').value;
|
|
196
|
+
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('on',x===t));
|
|
197
|
+
$('chatview').hidden=t.dataset.tab!=='chat';$('expview').hidden=t.dataset.tab!=='exp'});
|
|
198
|
+
$('euse').onclick=()=>$('etext').value=$('msg').value;
|
|
199
|
+
|
|
200
|
+
function chips(){$('chips').innerHTML=files.map((f,i)=>`<span class="chip">${esc(f.name)}<button data-i="${i}" aria-label="Remove ${esc(f.name)}">×</button></span>`).join('');
|
|
201
|
+
$('chips').querySelectorAll('button').forEach(b=>b.onclick=()=>{files.splice(+b.dataset.i,1);chips()})}
|
|
202
|
+
$('attach').onclick=()=>$('file').click();
|
|
203
|
+
$('file').onchange=e=>{files.push(...e.target.files);e.target.value='';chips()};
|
|
204
|
+
function trackPaste(e){const fs=[...(e.clipboardData?.files||[])];
|
|
205
|
+
if(fs.length){e.preventDefault();files.push(...fs);chips();return}
|
|
206
|
+
const t=e.clipboardData?.getData('text');if(t&&t.trim().length>=20)pasted.push(t)}
|
|
207
|
+
$('msg').addEventListener('paste',trackPaste);$('etext').addEventListener('paste',trackPaste);
|
|
208
|
+
$('msg').addEventListener('input',e=>{e.target.style.height='auto';e.target.style.height=Math.min(200,e.target.scrollHeight)+'px'});
|
|
209
|
+
$('msg').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
|
210
|
+
$('send').onclick=send;
|
|
211
|
+
if(window.palVoice)palVoice($('mic'),$('msg'),m=>{if(m)add('assistant','Pal',m)});
|
|
212
|
+
$('newchat').onclick=async()=>{await fetch('/api/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sid})});sid=sessionStorage.sid=crypto.randomUUID();$('msgs').innerHTML='';refreshSave()};
|
|
213
|
+
|
|
214
|
+
function add(cls,who,body){const d=document.createElement('div');d.className='msg '+cls;d.innerHTML=`<div class="who">${cls==='assistant'?'<img class="av" src="/static/pal-logo-32.png" alt="">':''}${who}</div><div class="body">${esc(body)}</div>`;$('msgs').appendChild(d);$('log').scrollTop=1e9;return d}
|
|
215
|
+
|
|
216
|
+
async function send(){
|
|
217
|
+
const text=$('msg').value.trim();if(!text&&!files.length)return;
|
|
218
|
+
const fd=new FormData();
|
|
219
|
+
fd.append('session_id',sid);fd.append('provider',$('prov').value);fd.append('model',$('model').value.trim());
|
|
220
|
+
fd.append('api_key',$('key').value);fd.append('message',text);fd.append('mode',mode);fd.append('route',$('route').value);fd.append('agent',$('agent').value);fd.append('pasted',JSON.stringify(pasted));
|
|
221
|
+
methods.forEach(m=>fd.append('methods',m));if(!methods.length)fd.append('methods','none');
|
|
222
|
+
files.forEach(f=>fd.append('files',f));
|
|
223
|
+
add('user','You',text+(files.length?`\nAttached: ${files.map(f=>f.name).join(', ')}`:''));
|
|
224
|
+
$('msg').value='';$('msg').style.height='auto';files=[];pasted=[];chips();
|
|
225
|
+
$('send').disabled=true;const w=add('assistant','Model','Working…');
|
|
226
|
+
try{
|
|
227
|
+
const r=await fetch('/api/chat',{method:'POST',body:fd});const d=await r.json();
|
|
228
|
+
if(!r.ok){w.querySelector('.body').innerHTML=`<span class="err">${esc(d.error)}</span>`;return}
|
|
229
|
+
const s=d.saver;
|
|
230
|
+
w.querySelector('.body').textContent=d.reply;
|
|
231
|
+
window.lastCtx=d.context||null;
|
|
232
|
+
if(d.fallback||(d.files&&d.files.length)){const n=document.createElement('div');n.style.cssText='font-size:12px;color:var(--muted);margin-top:6px';
|
|
233
|
+
if(d.fallback)n.append('Answered by the fallback model '+d.fallback.provider+'/'+d.fallback.model+' after '+d.fallback.reason+'. ');
|
|
234
|
+
(d.files||[]).forEach(p=>{const l=document.createElement('a');l.href='/api/workspace/file?session_id='+encodeURIComponent(sid)+'&path='+encodeURIComponent(p);l.textContent='Download '+p;l.setAttribute('download','');l.style.marginRight='10px';n.append(l)});
|
|
235
|
+
w.querySelector('.body').after(n)}
|
|
236
|
+
const errs=(d.file_errors||[]).map(e=>`<div class="err">${esc(e)}</div>`).join('');
|
|
237
|
+
const L=d.latency||{};
|
|
238
|
+
const lat=L.total!==undefined?` ${L.total}s total (local ${L.local}s, API ${L.api}s).`:'';
|
|
239
|
+
const bits=[];
|
|
240
|
+
if(d.context&&d.context.tokens_saved)bits.push(`history: sent ${d.context.turns_sent} of ${d.context.turns_total} earlier turns (~${d.context.tokens_saved} tok saved${d.context.recalled?`, ${d.context.recalled} recalled`:''})`);
|
|
241
|
+
if(d.graph)bits.push(`code graph: ~${d.graph.tokens} tok of excerpts instead of ~${d.graph.project_tokens}`);
|
|
242
|
+
if(d.documents&&d.documents.chunks)bits.push(`documents: ${d.documents.chunks} relevant parts (~${d.documents.tokens} tok) instead of ~${d.documents.indexed_tokens}`);
|
|
243
|
+
if(d.memory)bits.push(`memory: ${d.memory.notes} note(s) from earlier sessions`);
|
|
244
|
+
if(d.tools&&d.tools.length)bits.push(`tools: ${d.tools.map(t=>t.name+(t.ok?'':' (failed)')).join(', ')}`);
|
|
245
|
+
const extra=bits.length?`<div>${bits.map(esc).join('<br>')}</div>`:'';
|
|
246
|
+
const meta=d.handled_by==='tool'
|
|
247
|
+
?`Answered without a model (${esc(d.route_reason)}).${lat}`
|
|
248
|
+
:d.handled_by==='local'
|
|
249
|
+
?`Answered by the local LLM (${esc(d.route_reason)}). No API call. The API model sees this turn tagged [local-llm].${lat}`
|
|
250
|
+
:`API model. input ${d.input_tokens} tok, output ${d.output_tokens} tok. Savers cut ${s.tokens_saved} of ${s.tokens_original} tok in this message.${lat}${extra}
|
|
251
|
+
<details><summary>What was sent</summary><pre>${esc(d.sent)}</pre></details>`;
|
|
252
|
+
w.insertAdjacentHTML('beforeend',`${errs}<div class="meta">${meta}</div>`);
|
|
253
|
+
if(s&&!$('etext').value)$('etext').value=s.original;
|
|
254
|
+
}catch(e){w.querySelector('.body').innerHTML=`<span class="err">${esc(e.message)}</span>`}
|
|
255
|
+
finally{$('send').disabled=false;$('log').scrollTop=1e9;refreshSave()}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function applyAgent(){const on=!!$('agent').value;if(!$('msg').dataset.ph)$('msg').dataset.ph=$('msg').placeholder;sessionStorage.agent=$('agent').value;
|
|
259
|
+
['prov','model','route'].forEach(i=>{$(i).disabled=on;$(i).style.opacity=on?.45:1});
|
|
260
|
+
$('msg').placeholder=on?($('agent').selectedOptions[0].title||'Ask the agent'):$('msg').dataset.ph}
|
|
261
|
+
$('agent').onchange=applyAgent;
|
|
262
|
+
async function refreshSave(){
|
|
263
|
+
try{const m=(await (await fetch('/api/metrics?session='+encodeURIComponent(sid))).json()).summary;
|
|
264
|
+
if(!m.turns){$('sv1').textContent='0';$('sv2').textContent='0';$('sv3').textContent='API used: 0 in / 0 out';return}
|
|
265
|
+
$('sv1').textContent=m.tokens_saved_estimate.total.toLocaleString();$('sv2').textContent=m.api_calls_avoided;
|
|
266
|
+
$('sv3').textContent=`API used: ${m.api_input_tokens.toLocaleString()} in / ${m.api_output_tokens.toLocaleString()} out`}
|
|
267
|
+
catch(e){}}
|
|
268
|
+
refreshSave();
|
|
269
|
+
|
|
270
|
+
const fmt=n=>Number(n).toLocaleString();
|
|
271
|
+
const trunc=(t,n)=>t.length>n?t.slice(0,n-1)+'…':t;
|
|
272
|
+
async function postJson(url,body){const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
273
|
+
let d={};try{d=await r.json()}catch(e){}return {ok:r.ok,d}}
|
|
274
|
+
function demoRow(i,st){return document.getElementById('dr'+i)}
|
|
275
|
+
function fillRow(r){
|
|
276
|
+
const n=r.naive,m=r.pal;
|
|
277
|
+
const label=m.route==='api'?'<span class="badge">API model</span>':m.route==='local'?'<span class="badge local">local model</span>':'<span class="badge tool">no model</span>';
|
|
278
|
+
const extra=[m.reduced_tokens_saved?`input cut ${m.reduced_tokens_saved}`:'',m.document_chunks?`${m.document_chunks} doc part(s)`:'',m.tools.length?`tool: ${m.tools.join(', ')}`:''].filter(Boolean).join(', ');
|
|
279
|
+
const el=document.getElementById('dr'+r.step);el.className='';
|
|
280
|
+
el.innerHTML=`<td>${r.step}</td><td>${esc(trunc(r.message,70))}</td><td>${label}${extra?`<div class="note">${esc(extra)}</div>`:''}</td><td class="num">${fmt(n.input_tokens)} / ${fmt(n.output_tokens)}</td><td class="num">${fmt(m.input_tokens)} / ${fmt(m.output_tokens)}</td><td class="num">${n.latency.toFixed(1)}</td><td class="num">${m.latency.toFixed(1)}</td>`}
|
|
281
|
+
function showSummary(s){
|
|
282
|
+
const lat=s.latency_saved,latGood=lat>=0;
|
|
283
|
+
const notes=s.notes.map(x=>`<div class="note">${esc(x)}</div>`).join('');
|
|
284
|
+
$('dsum').innerHTML=`<div class="cards">
|
|
285
|
+
<div class="card"><div class="lab">Tokens saved (API-reported, all 6 messages)</div><div class="big ${s.tokens_saved>=0?'':'bad'}">${fmt(s.tokens_saved)} <span style="font-size:16px">(${s.tokens_saved_pct}%)</span></div>
|
|
286
|
+
<div class="sub">Plain ${fmt(s.naive.total_tokens)} tokens (${fmt(s.naive.input_tokens)} in, ${fmt(s.naive.output_tokens)} out)<br>Pal ${fmt(s.pal.total_tokens)} tokens (${fmt(s.pal.input_tokens)} in, ${fmt(s.pal.output_tokens)} out)<br>Input saved ${fmt(s.input_saved)}, output saved ${fmt(s.output_saved)}</div></div>
|
|
287
|
+
<div class="card"><div class="lab">Latency ${latGood?'saved':'added'} (total for the 6 messages)</div><div class="big ${latGood?'':'bad'}">${Math.abs(lat).toFixed(1)} s <span style="font-size:16px">(${Math.abs(s.latency_saved_pct)}%)</span></div>
|
|
288
|
+
<div class="sub">Plain ${s.naive.latency.toFixed(1)} s<br>Pal ${s.pal.latency.toFixed(1)} s, local model time included</div></div>
|
|
289
|
+
<div class="card"><div class="lab">What Pal did</div><div class="big" style="font-size:22px">${s.api_calls_avoided} of 6 API calls avoided</div>
|
|
290
|
+
<div class="sub">${s.pal.direct_answers} answered with no model, ${s.pal.local_answers} by the local model, ${s.pal.api_calls} by the API model${s.pal.tool_calls?`, ${s.pal.tool_calls} tool call(s)`:''}<br>Local model: ${esc(s.local.detail||'unknown')}</div></div></div>
|
|
291
|
+
${notes}
|
|
292
|
+
<h2 class="mt">What we made</h2><div class="note">Pal's answer to the last message, built from all six.</div>
|
|
293
|
+
<div class="result" id="dresult">${esc(s.result)}</div>
|
|
294
|
+
<div class="opts"><button id="dcopy">Copy</button></div>
|
|
295
|
+
<details><summary>The same last message answered without Pal</summary><div class="result mt">${esc(s.result_naive)}</div></details>`;
|
|
296
|
+
$('dcopy').onclick=()=>navigator.clipboard.writeText(s.result).then(()=>$('dcopy').textContent='Copied')}
|
|
297
|
+
$('drun').onclick=async()=>{
|
|
298
|
+
$('drun').disabled=true;$('dsum').innerHTML='';$('dstat').classList.remove('err');$('dstat').textContent='Starting…';
|
|
299
|
+
const body={provider:$('prov').value,model:$('model').value.trim(),api_key:$('key').value};
|
|
300
|
+
const st=await postJson('/api/demo/start',body);
|
|
301
|
+
if(!st.ok){$('dstat').textContent=st.d.error||'could not start';$('dstat').classList.add('err');$('drun').disabled=false;return}
|
|
302
|
+
const run=st.d.run_id;
|
|
303
|
+
$('dtable').hidden=false;
|
|
304
|
+
$('dbody').innerHTML=st.d.steps.map((x,i)=>`<tr id="dr${i+1}"><td>${i+1}</td><td>${esc(trunc(x.message,70))}${x.attachment?` <span class="badge">+ ${esc(x.attachment)}</span>`:''}</td><td colspan="5" class="note">waiting</td></tr>`).join('');
|
|
305
|
+
for(let i=1;i<=st.d.steps.length;i++){
|
|
306
|
+
$('dstat').textContent=`Message ${i} of ${st.d.steps.length}…`;
|
|
307
|
+
document.getElementById('dr'+i).className='run';document.getElementById('dr'+i).lastElementChild.textContent='running…';
|
|
308
|
+
const r=await postJson('/api/demo/step',{run_id:run,api_key:$('key').value});
|
|
309
|
+
if(!r.ok){$('dstat').textContent=`Stopped at message ${i}: ${r.d.error||'error'}`;$('dstat').classList.add('err');
|
|
310
|
+
await postJson('/api/demo/cancel',{run_id:run});$('drun').disabled=false;return}
|
|
311
|
+
fillRow(r.d.row);
|
|
312
|
+
if(r.d.done){showSummary(r.d.summary);$('dstat').textContent='Done.'}
|
|
313
|
+
}
|
|
314
|
+
$('drun').disabled=false};
|
|
315
|
+
|
|
316
|
+
async function loadStats(){
|
|
317
|
+
try{const d=await (await fetch('/api/metrics')).json();const m=d.summary;
|
|
318
|
+
const mem=d.memory||{},nt=mem.notes||{},pr=mem.process||{},memLine=mem.notes?`memory: ${nt.notes} note(s) (${nt.ram_count} in RAM, ${nt.disk_count} on disk, RAM cache ${Math.round((nt.usage_ratio||0)*100)}% full), ${(mem.sessions||{}).open} open chat(s), ${(mem.knowledge_graph||{}).projects} indexed project(s), storage ${(((mem.storage||{}).bytes||0)/1048576).toFixed(1)} MB, this process ${pr.rss_mb??pr.peak_rss_mb??'?'} MB`:'';
|
|
319
|
+
if(!m.turns){$('statbox').textContent=['No turns yet.',memLine].filter(Boolean).join('\n');return}
|
|
320
|
+
const r=m.by_route,t=m.tokens_saved_estimate;
|
|
321
|
+
$('statbox').textContent=[
|
|
322
|
+
`turns ${m.turns}: API ${r.api.turns}, local ${r.local.turns}, no-model ${r.tool.turns} (API calls avoided: ${m.api_calls_avoided})`,
|
|
323
|
+
`latency p50/p95: API ${r.api.p50_latency_sec}s/${r.api.p95_latency_sec}s, local ${r.local.p50_latency_sec}s/${r.local.p95_latency_sec}s`,
|
|
324
|
+
`local overhead on API turns: ${m.avg_local_overhead_sec_on_api_turns}s avg`,
|
|
325
|
+
m.latency_stages&&Object.keys(m.latency_stages).length?`where the time goes: ${Object.entries(m.latency_stages).sort((a,b)=>b[1].total_sec-a[1].total_sec).map(([k,v])=>`${k.replace('_',' ')} ${v.share_pct}%`).join(', ')} (slowest: ${(m.slowest_stage||'').replace('_',' ')})`:'',
|
|
326
|
+
`compression: ${m.compressed_turns} old replies shown compressed, about ${m.compression_saved} tokens saved (no model used)`,
|
|
327
|
+
window.lastCtx&&window.lastCtx.history_tokens?`what is big in this chat: your messages ${window.lastCtx.history_tokens.user}, assistant prose ${window.lastCtx.history_tokens.assistant_prose}, assistant code ${window.lastCtx.history_tokens.assistant_code} tokens`:'',
|
|
328
|
+
`queue waits (avg): local model ${m.avg_wait_local_sec}s, API line ${m.avg_wait_api_sec}s. Local jobs paused for a better job: ${m.local_pauses}`,
|
|
329
|
+
`cost: $${m.cost_usd} spent; about $${m.saved_cost_usd} saved by the token savers (LiteLLM price table); answered by the fallback model: ${m.fallbacks}`,
|
|
330
|
+
`API tokens: ${m.api_input_tokens} in, ${m.api_output_tokens} out`,
|
|
331
|
+
`saved (estimate): ${t.total} tok = savers ${t.input_savers}, history ${t.context_paging}, code graph ${t.code_graph}, documents ${t.documents}`,
|
|
332
|
+
`tool calls ${m.tool_calls}, memory notes used ${m.memory_hits}, escalations ${m.escalations}, errors ${m.errors}`,memLine].filter(Boolean).join('\n')}
|
|
333
|
+
catch(e){$('statbox').textContent='Could not load stats.'}}
|
|
334
|
+
async function loadMem(){
|
|
335
|
+
try{const d=await (await fetch('/api/memory')).json();$('memnote').textContent=d.enabled?`${d.notes} note(s) stored`+(d.stats?` (${d.stats.ram_count} in RAM, ${d.stats.disk_count} on disk).`:'.'):'Memory is off in the settings.'}
|
|
336
|
+
catch(e){$('memnote').textContent='Could not load.'}}
|
|
337
|
+
$('statsum').parentElement.addEventListener('toggle',e=>{if(e.target.open)loadStats()});
|
|
338
|
+
$('memsum').parentElement.addEventListener('toggle',e=>{if(e.target.open)loadMem()});
|
|
339
|
+
$('statrefresh').onclick=loadStats;
|
|
340
|
+
$('statreset').onclick=async()=>{await fetch('/api/metrics/reset',{method:'POST'});loadStats()};
|
|
341
|
+
$('memclear').onclick=async()=>{if(confirm('Delete all stored memory?')){await fetch('/api/memory',{method:'DELETE'});loadMem()}};
|
|
342
|
+
|
|
343
|
+
$('erun').onclick=async()=>{
|
|
344
|
+
const out=$('eout');out.textContent='Running…';$('erun').disabled=true;
|
|
345
|
+
try{
|
|
346
|
+
const r=await fetch('/api/experiment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
|
347
|
+
text:$('etext').value,methods,mode,pasted,provider:$('prov').value,model:$('model').value.trim(),api_key:$('key').value,call_api:$('ecall').checked})});
|
|
348
|
+
const d=await r.json();if(!r.ok){out.innerHTML=`<span class="err">${esc(d.error)}</span>`;return}
|
|
349
|
+
const s=d.saver,pct=s.tokens_original?Math.round(100*s.tokens_saved/s.tokens_original):0;
|
|
350
|
+
let h=`<div class="sum">${s.tokens_original} to ${s.tokens_reduced} tokens, <b>saved ${s.tokens_saved} (${pct}%)</b></div>
|
|
351
|
+
<table><tr><th>Method</th><th>Result</th><th>Tokens</th></tr>`+s.steps.map((st,i)=>`
|
|
352
|
+
<tr class="step" data-i="${i}"><td>${esc(st.name)}</td><td><span class="st ${st.status}">${st.status}</span><div class="note">${esc(st.reason)}</div></td>
|
|
353
|
+
<td>${st.tokens_before} to ${st.tokens_after}<div class="bar"><i style="width:${s.tokens_original?100*st.tokens_after/s.tokens_original:0}%"></i></div></td></tr>
|
|
354
|
+
<tr hidden id="d${i}"><td colspan="3"><div class="cmp"><div>before<pre>${esc(st.before)}</pre></div><div>after<pre>${esc(st.after)}</pre></div></div></td></tr>`).join('')+`</table>
|
|
355
|
+
<div class="note">Token counter: ${esc(d.counter)}. Click a row to compare the text before and after that method.</div>`;
|
|
356
|
+
if(d.api){const a=d.api;h+=`<h2 style="margin-top:18px">API, raw vs saved</h2><table><tr><th></th><th>Input</th><th>Output</th></tr>
|
|
357
|
+
<tr><td>Raw prompt, no output config</td><td>${a.raw.input_tokens}</td><td>${a.raw.output_tokens}</td></tr>
|
|
358
|
+
<tr><td>Reduced prompt + output config</td><td>${a.optimized.input_tokens}</td><td>${a.optimized.output_tokens}</td></tr></table>
|
|
359
|
+
<div class="cmp"><div>raw reply<pre>${esc(a.raw.reply)}</pre></div><div>saved reply<pre>${esc(a.optimized.reply)}</pre></div></div>
|
|
360
|
+
<div class="note">Counts are the API's own usage report. The output config adds input tokens to every request; it pays off when replies get shorter.</div>`}
|
|
361
|
+
out.innerHTML=h;
|
|
362
|
+
out.querySelectorAll('tr.step').forEach(tr=>tr.onclick=()=>{const x=$('d'+tr.dataset.i);x.hidden=!x.hidden});
|
|
363
|
+
}catch(e){out.innerHTML=`<span class="err">${esc(e.message)}</span>`}
|
|
364
|
+
finally{$('erun').disabled=false}
|
|
365
|
+
};
|
|
366
|
+
</script>
|
|
367
|
+
</body>
|
|
368
|
+
</html>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Pal Studio</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/static/pal-logo-32.png">
|
|
8
|
+
<link rel="apple-touch-icon" href="/static/pal-logo-256.png">
|
|
9
|
+
<link rel="stylesheet" href="/static/inter.css">
|
|
10
|
+
<link rel="stylesheet" href="/static/studio.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<noscript><p class="warn">This page needs JavaScript.</p></noscript>
|
|
14
|
+
<div class="shell">
|
|
15
|
+
<aside id="side" class="side" aria-label="Navigation">
|
|
16
|
+
<div class="brand"><img class="mark" src="/static/pal-logo-64.png" alt="" width="28" height="28"><span class="brandname">Pal</span></div>
|
|
17
|
+
<nav>
|
|
18
|
+
<button id="newbuild" class="navitem active" type="button"><span class="ico" aria-hidden="true">+</span> New build</button>
|
|
19
|
+
<a class="navitem" href="/"><span class="ico" aria-hidden="true">✉</span> Chat</a>
|
|
20
|
+
<div class="navhead">Recents</div>
|
|
21
|
+
<div id="runs" class="runs"><p class="navnote">Nothing built yet.</p></div>
|
|
22
|
+
</nav>
|
|
23
|
+
<div class="sidefoot">
|
|
24
|
+
<span id="localchip" class="chip" role="status">Checking the local model...</span>
|
|
25
|
+
</div>
|
|
26
|
+
</aside>
|
|
27
|
+
|
|
28
|
+
<section class="mainwrap is-empty">
|
|
29
|
+
<header class="top">
|
|
30
|
+
<button id="menu" class="iconbtn menu" type="button" aria-label="Show or hide the menu" aria-expanded="false">☰</button>
|
|
31
|
+
<span class="toptitle">Studio</span>
|
|
32
|
+
<nav class="views" aria-label="View">
|
|
33
|
+
<button id="viewchat" class="active" type="button">Chat</button>
|
|
34
|
+
<button id="vieweditor" type="button" disabled title="Build something first, then open it here">Editor</button>
|
|
35
|
+
</nav>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<main id="main" class="main" aria-label="Conversation">
|
|
39
|
+
<div id="empty" class="empty">
|
|
40
|
+
<img class="spark" src="/static/pal-logo-256.png" alt="" width="72" height="72">
|
|
41
|
+
<h1>What do you want to build?</h1>
|
|
42
|
+
</div>
|
|
43
|
+
<div id="thread" class="thread" aria-live="polite" hidden></div>
|
|
44
|
+
</main>
|
|
45
|
+
|
|
46
|
+
<form id="composer" class="composer" autocomplete="off">
|
|
47
|
+
<div id="refchips" class="refchips" hidden></div>
|
|
48
|
+
<label for="cmd" class="sr">Describe what you want built, or tell me what to change</label>
|
|
49
|
+
<textarea id="cmd" rows="1" maxlength="4000" placeholder="Describe what you want built, or tell me what to change"></textarea>
|
|
50
|
+
<div class="bar">
|
|
51
|
+
<button id="attach" class="iconbtn" type="button" title="Attach reference files (text files, up to 150 KB each)" aria-label="Attach files">+</button>
|
|
52
|
+
<input id="refs" type="file" multiple hidden accept=".txt,.md,.json,.html,.css,.js,.py,.csv,.xml,.yml,.yaml,.toml">
|
|
53
|
+
<label class="modelpick"><span class="sr">Bot model</span><select id="botmodel" title="The model that plans with you. The strongest plans best but is slowest."></select></label>
|
|
54
|
+
<span class="sp"></span>
|
|
55
|
+
<button id="mic" class="iconbtn" type="button" title="Speak instead of typing" aria-label="Speak instead of typing" aria-pressed="false">
|
|
56
|
+
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3zm5-3a5 5 0 0 1-10 0H5a7 7 0 0 0 6 6.92V22h2v-3.08A7 7 0 0 0 19 12h-2z" fill="currentColor"/></svg>
|
|
57
|
+
</button>
|
|
58
|
+
<button id="send" class="sendbtn" type="submit" title="Send" aria-label="Send" disabled>
|
|
59
|
+
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 5l-6 6h4v8h4v-8h4z" fill="currentColor"/></svg>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<p id="voicehint" class="hint" hidden></p>
|
|
63
|
+
</form>
|
|
64
|
+
<div id="below" class="below">
|
|
65
|
+
<div id="suggest" class="suggest" aria-label="Examples"></div>
|
|
66
|
+
<button id="byhand" class="linklike" type="button">or set up by hand</button>
|
|
67
|
+
<p class="hint">Local model: <span id="startmodel">checking...</span>. Everything stays on this machine unless you choose an API model.</p>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
71
|
+
<section id="editor" class="editor" aria-label="Code editor" hidden>
|
|
72
|
+
<aside class="ed-files" aria-label="Project files">
|
|
73
|
+
<div class="ed-h">Files</div>
|
|
74
|
+
<ul id="edfiles" class="edfiles"></ul>
|
|
75
|
+
<form id="ednew" class="ednew" autocomplete="off">
|
|
76
|
+
<label class="sr" for="ednewname">New file name</label>
|
|
77
|
+
<input id="ednewname" type="text" maxlength="120" placeholder="new file, e.g. app.js">
|
|
78
|
+
<button type="submit" class="iconbtn" title="Create the file" aria-label="Create the file">+</button>
|
|
79
|
+
</form>
|
|
80
|
+
<a id="eddownload" class="linkbtn" href="#">Download (.zip)</a>
|
|
81
|
+
</aside>
|
|
82
|
+
<div class="ed-center">
|
|
83
|
+
<div id="edtabs" class="ed-tabs" role="tablist"></div>
|
|
84
|
+
<div class="ed-pane">
|
|
85
|
+
<p id="edempty" class="edempty">Open a file from the list.</p>
|
|
86
|
+
<pre id="edgutter" class="gutter" aria-hidden="true" hidden></pre>
|
|
87
|
+
<textarea id="edcode" class="code" spellcheck="false" wrap="off" aria-label="Code" hidden></textarea>
|
|
88
|
+
<div id="eddiff" class="ed-diff" hidden></div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="ed-bar">
|
|
91
|
+
<span id="edpath" class="edpath"></span>
|
|
92
|
+
<span class="sp"></span>
|
|
93
|
+
<span id="edstatus" class="edstatus" role="status"></span>
|
|
94
|
+
<span id="edpos" class="edpos"></span>
|
|
95
|
+
<button id="edsave" class="btn" type="button" disabled>Save</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<aside class="ed-ask" aria-label="Ask Pal">
|
|
99
|
+
<div class="ed-h"><img class="mark" src="/static/pal-logo-32.png" alt="" width="20" height="20"> Ask Pal</div>
|
|
100
|
+
<div id="edmsgs" class="edmsgs" aria-live="polite"></div>
|
|
101
|
+
<p id="edmeter" class="edmeter"></p>
|
|
102
|
+
<form id="edform" class="edform" autocomplete="off">
|
|
103
|
+
<label class="sr" for="edask">Ask Pal to change the open file</label>
|
|
104
|
+
<textarea id="edask" rows="2" maxlength="4000" placeholder="Change this file, add a feature, or ask a question"></textarea>
|
|
105
|
+
<div class="bar">
|
|
106
|
+
<button id="edstop" class="btn" type="button" hidden>Stop</button>
|
|
107
|
+
<span class="sp"></span>
|
|
108
|
+
<button id="edsend" class="sendbtn" type="submit" title="Send" aria-label="Send" disabled>
|
|
109
|
+
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 5l-6 6h4v8h4v-8h4z" fill="currentColor"/></svg>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</form>
|
|
113
|
+
</aside>
|
|
114
|
+
</section>
|
|
115
|
+
</div>
|
|
116
|
+
<script src="/static/voice.js"></script>
|
|
117
|
+
<script src="/static/studio.js"></script>
|
|
118
|
+
<script src="/static/editor.js"></script>
|
|
119
|
+
</body>
|
|
120
|
+
</html>
|