diff --git a/README.md b/README.md index 04d3991..2260210 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,11 @@ All releases, with checksums: [conductloop-releases](https://github.com/EVMCOD/c - **Backups** of the local database, daily and before every upgrade, restorable from Setup. - **It learns from your 👍/👎**: proposed rules that you approve, all stored on your computer. -The screenshots use a fictional vault. Dark theme: ◐ in the corner. +| Ask the team | The CEO's answer | +|---|---| +|  |  | + +The screenshots use a fictional vault; the team ones ran on a real model (MiniMax-M3). Dark theme: ◐ in the corner. --- diff --git a/docs/screenshots/08-team-answer.png b/docs/screenshots/08-team-answer.png new file mode 100644 index 0000000..3ff8f25 Binary files /dev/null and b/docs/screenshots/08-team-answer.png differ diff --git a/docs/screenshots/09-team-cards.png b/docs/screenshots/09-team-cards.png new file mode 100644 index 0000000..e353204 Binary files /dev/null and b/docs/screenshots/09-team-cards.png differ diff --git a/docs/screenshots/10-brain-answer.png b/docs/screenshots/10-brain-answer.png new file mode 100644 index 0000000..9c00f23 Binary files /dev/null and b/docs/screenshots/10-brain-answer.png differ diff --git a/ops_dashboard/brain.py b/ops_dashboard/brain.py index fc75d60..5741f34 100644 --- a/ops_dashboard/brain.py +++ b/ops_dashboard/brain.py @@ -219,7 +219,7 @@ def _local_endpoint(base_url: str | None) -> bool: - A cross-source TODO/pendiente list. You are direct, concise, and opinionated. Match the user's language. When you -cite a fact, append the source like [obsidian:filename.md] or [repo:name]. +cite a fact from the context excerpts, append the excerpt's number, like [1]. Don't make up information. If you don't know, say so. Today's date: {today} diff --git a/ops_dashboard/server.py b/ops_dashboard/server.py index 0e5ba51..1f5182f 100644 --- a/ops_dashboard/server.py +++ b/ops_dashboard/server.py @@ -2622,7 +2622,9 @@ def _brain_ask_tools(self) -> None: today = time.strftime("%Y-%m-%d") prefs = db.memory_list() pref_str = "\n".join(f"- {p['key']}: {p['value']}" for p in prefs) or "(none yet)" - system = brain.SYSTEM_PROMPT_BASE.format(today=today, preferences=pref_str) + system = brain.SYSTEM_PROMPT_BASE.format(today=today, preferences=pref_str) + ( + "\n\nThere are no numbered excerpts here: cite facts from tool results by " + "source, like [obsidian:filename.md] or [repo:name].") messages = [{"role": "user", "content": q}] tool_log = [] def on_call(name, args, result): diff --git a/ops_dashboard/web/chat.js b/ops_dashboard/web/chat.js index 6c540bf..c483c15 100644 --- a/ops_dashboard/web/chat.js +++ b/ops_dashboard/web/chat.js @@ -58,8 +58,43 @@ return '' + text + ''; }); s = s.replace(/\[(\d{1,2})\]/g, '$1'); + // Some models cite by source ("[obsidian:Pricing.md]") instead of [N]: show the name as a chip. + s = s.replace(/\[(obsidian|repo|memory|web|voice|mcp):([^\]\n]{1,120})\]/g, function (_m, kind, name) { + var label = name.split('/').pop().replace(/\.md$/i, ''); + return '' + label + ''; + }); return s; } + var LIST = /^(\s*)([-*•]|\d+[.)])\s+(.*)$/; + // One list starting at lines[i]. Deeper-indented items nest inside the item + // above, and an ordered list keeps its first number, so "1. … - note … 2. …" + // doesn't restart at 1. + function list(lines, i, indent) { + var first = LIST.exec(lines[i]), ordered = /\d/.test(first[2]), items = []; + while (i < lines.length) { + if (!lines[i].trim() && i + 1 < lines.length) { + var next = LIST.exec(lines[i + 1]); + if (next && next[1].length >= indent) { i++; continue; } + break; + } + var m = LIST.exec(lines[i]); + if (!m || m[1].length < indent) break; + if (m[1].length > indent && items.length) { + var sub = list(lines, i, m[1].length); + items[items.length - 1] += sub.html; + i = sub.i; + continue; + } + if (/\d/.test(m[2]) !== ordered) break; + items.push('
' + para.map(inline).join('
') + '
' + inline(line.replace(/^>\s?/, '')) + ''); i++; continue; } diff --git a/tests/test_chat_markdown_js.py b/tests/test_chat_markdown_js.py new file mode 100644 index 0000000..5a6d1a9 --- /dev/null +++ b/tests/test_chat_markdown_js.py @@ -0,0 +1,55 @@ +"""Unit test for the markdown renderer in web/chat.js (esc → inline → list → +md). Pulls those functions out of the IIFE and runs them under Node; skipped +when Node isn't installed.""" +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +CHAT = Path(__file__).resolve().parent.parent / "ops_dashboard" / "web" / "chat.js" +NODE = shutil.which("node") + +CASES = { + # A bullet between numbered items used to close the
Fact 2.
' + # Escaped before formatting: the only tags are ours, the name stays text. + assert re.findall(r"<(\w+)", out["escaped"]) == ["p", "span"] + assert out["escaped"] == ('' + "<img src=x onerror=alert(1)>
")