From 5799be543bde4a42fcd4b8e70f7cd8194c53df34 Mon Sep 17 00:00:00 2001 From: Rick Ye Date: Sun, 6 Sep 2026 10:01:13 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20scripts=20=E4=B8=8E=20preview=20?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E5=B7=A5=E5=85=B7=E7=A7=BB=E5=87=BA=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=B7=9F=E8=B8=AA=EF=BC=88.gitignore=20=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + apps/desktop/preview/index.html | 12 - apps/desktop/preview/main.tsx | 210 ------ apps/desktop/preview/preview.css | 6 - apps/desktop/scripts/capture-app.cjs | 263 ------- apps/desktop/scripts/capture-preview.cjs | 95 --- apps/desktop/scripts/debug-collapse.cjs | 21 - apps/desktop/scripts/debug-preview.cjs | 41 -- apps/desktop/scripts/inspect-bundle.cjs | 17 - apps/desktop/scripts/make-icons.cjs | 87 --- apps/desktop/scripts/read-dts.cjs | 15 - apps/desktop/scripts/read-dts2.cjs | 22 - .../desktop/scripts/repro-empty-workspace.mjs | 21 - apps/desktop/scripts/repro-event-flow.mjs | 103 --- apps/desktop/scripts/repro-markdown.mjs | 41 -- apps/desktop/scripts/repro-render-logic.mjs | 81 --- apps/desktop/scripts/smoke-models.mjs | 65 -- apps/desktop/scripts/smoke-sessions.mjs | 76 --- apps/desktop/scripts/smoke-tools.mjs | 66 -- apps/desktop/scripts/smoke.mjs | 39 -- scripts/beautifului/approval-card.json | 20 - scripts/beautifului/button.json | 21 - scripts/beautifului/foundation.json | 15 - scripts/beautifului/glide-menu.json | 18 - scripts/beautifului/loading-state.json | 18 - scripts/beautifului/prompt-bar.json | 21 - scripts/beautifului/registry.json | 142 ---- scripts/beautifului/selection-actions.json | 24 - scripts/beautifului/shimmer.json | 18 - scripts/beautifului/sidebar-nav.json | 28 - scripts/beautifului/stage/ApprovalCard.tsx | 419 ------------ scripts/beautifului/stage/Button.tsx | 47 -- scripts/beautifului/stage/GlideMenu.tsx | 59 -- scripts/beautifului/stage/LoadingState.tsx | 152 ----- scripts/beautifului/stage/SidebarNav.tsx | 449 ------------ scripts/beautifului/stage/StreamingText.tsx | 252 ------- scripts/beautifului/stage/ThinkingState.tsx | 273 -------- scripts/beautifului/stage/ToolChips.tsx | 336 --------- scripts/beautifului/stage/foundation.css | 643 ------------------ scripts/beautifului/stage/sidebar-nav.css | 81 --- scripts/beautifului/stream-text.json | 18 - scripts/beautifului/streaming-text.json | 18 - scripts/beautifului/task-rows.json | 18 - scripts/beautifului/thinking-state.json | 18 - scripts/beautifului/tool-chips.json | 18 - scripts/check-lu-icons.mjs | 18 - scripts/extract-beautifului.mjs | 52 -- scripts/fetch-beautifului.mjs | 47 -- scripts/install-beautifului.mjs | 20 - scripts/probe-pi-sdk.mjs | 32 - 50 files changed, 2 insertions(+), 4576 deletions(-) delete mode 100644 apps/desktop/preview/index.html delete mode 100644 apps/desktop/preview/main.tsx delete mode 100644 apps/desktop/preview/preview.css delete mode 100644 apps/desktop/scripts/capture-app.cjs delete mode 100644 apps/desktop/scripts/capture-preview.cjs delete mode 100644 apps/desktop/scripts/debug-collapse.cjs delete mode 100644 apps/desktop/scripts/debug-preview.cjs delete mode 100644 apps/desktop/scripts/inspect-bundle.cjs delete mode 100644 apps/desktop/scripts/make-icons.cjs delete mode 100644 apps/desktop/scripts/read-dts.cjs delete mode 100644 apps/desktop/scripts/read-dts2.cjs delete mode 100644 apps/desktop/scripts/repro-empty-workspace.mjs delete mode 100644 apps/desktop/scripts/repro-event-flow.mjs delete mode 100644 apps/desktop/scripts/repro-markdown.mjs delete mode 100644 apps/desktop/scripts/repro-render-logic.mjs delete mode 100644 apps/desktop/scripts/smoke-models.mjs delete mode 100644 apps/desktop/scripts/smoke-sessions.mjs delete mode 100644 apps/desktop/scripts/smoke-tools.mjs delete mode 100644 apps/desktop/scripts/smoke.mjs delete mode 100644 scripts/beautifului/approval-card.json delete mode 100644 scripts/beautifului/button.json delete mode 100644 scripts/beautifului/foundation.json delete mode 100644 scripts/beautifului/glide-menu.json delete mode 100644 scripts/beautifului/loading-state.json delete mode 100644 scripts/beautifului/prompt-bar.json delete mode 100644 scripts/beautifului/registry.json delete mode 100644 scripts/beautifului/selection-actions.json delete mode 100644 scripts/beautifului/shimmer.json delete mode 100644 scripts/beautifului/sidebar-nav.json delete mode 100644 scripts/beautifului/stage/ApprovalCard.tsx delete mode 100644 scripts/beautifului/stage/Button.tsx delete mode 100644 scripts/beautifului/stage/GlideMenu.tsx delete mode 100644 scripts/beautifului/stage/LoadingState.tsx delete mode 100644 scripts/beautifului/stage/SidebarNav.tsx delete mode 100644 scripts/beautifului/stage/StreamingText.tsx delete mode 100644 scripts/beautifului/stage/ThinkingState.tsx delete mode 100644 scripts/beautifului/stage/ToolChips.tsx delete mode 100644 scripts/beautifului/stage/foundation.css delete mode 100644 scripts/beautifului/stage/sidebar-nav.css delete mode 100644 scripts/beautifului/stream-text.json delete mode 100644 scripts/beautifului/streaming-text.json delete mode 100644 scripts/beautifului/task-rows.json delete mode 100644 scripts/beautifului/thinking-state.json delete mode 100644 scripts/beautifului/tool-chips.json delete mode 100644 scripts/check-lu-icons.mjs delete mode 100644 scripts/extract-beautifului.mjs delete mode 100644 scripts/fetch-beautifului.mjs delete mode 100644 scripts/install-beautifului.mjs delete mode 100644 scripts/probe-pi-sdk.mjs diff --git a/.gitignore b/.gitignore index f499a46..0d81b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ release/ preview-shots/ *.tsbuildinfo capture-log.txt +scripts/ +preview/ diff --git a/apps/desktop/preview/index.html b/apps/desktop/preview/index.html deleted file mode 100644 index 38f5a77..0000000 --- a/apps/desktop/preview/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - beautifului primitives preview - - -
- - - diff --git a/apps/desktop/preview/main.tsx b/apps/desktop/preview/main.tsx deleted file mode 100644 index e567358..0000000 --- a/apps/desktop/preview/main.tsx +++ /dev/null @@ -1,210 +0,0 @@ -// beautifului.dev 组件视觉验收页(静态构建,模拟聊天流内的真实用法) -import { useRef, useState } from 'react' -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './preview.css' -import { Thinking } from '../src/renderer/src/components/Thinking' -import { StreamingText } from '../src/renderer/src/components/StreamingText' -import { ApprovalStack } from '../src/renderer/src/components/ApprovalCard' -import { ToolChips } from '../src/renderer/src/components/ToolChips' -import { SidebarNav } from '../src/renderer/src/components/SidebarNav' -import { Search } from '../src/renderer/src/components/Search' -import { CodeBlock } from '../src/renderer/src/components/CodeBlock' -import { SelectionActions } from '../src/renderer/src/components/SelectionActions' -import { Loading } from '../src/renderer/src/components/Loading' -import { Markdown } from '../src/renderer/src/components/Markdown' -import type { ApprovalRequest, SessionMeta } from '../src/shared/types' - -const thinkingRows = [ - { primary: '用户想了解今天的品类热点,我先查一下最近的趋势数据。' }, - { primary: '需要调用 query_trend 工具获取近 7 天的热点词,再按类目分组。' }, - { primary: '结果里先给 TOP3 品类,附上建议的标题关键词。' } -] - -const toolCalls = [ - { - id: 'call-1', - name: 'query_trend', - tool: { - toolName: 'query_trend', - args: { category: 'home', days: 7 }, - status: 'done' as const, - result: { content: [{ type: 'text', text: '{ "top": ["收纳", "香薰", "桌布"], "lift": [0.32, 0.21, 0.18] }' }] } - } - }, - { - id: 'call-2', - name: 'draft_title', - tool: { - toolName: 'draft_title', - args: { keywords: ['收纳', '灵感'] }, - status: 'error' as const, - result: { content: [{ type: 'text', text: 'Error: rate limited, retry after 30s' }] } - } - }, - { - id: 'call-3', - name: 'update_price', - pendingApproval: { args: { sku: 'SKU-1024', from: 39.9, to: 35.9 } } - } -] - -const approvalRequest: ApprovalRequest = { - id: 'approval-1', - toolName: 'update_price', - args: { sku: 'SKU-1024', from: 39.9, to: 35.9 } -} - -const sessions: SessionMeta[] = [ - { id: 's1', path: 'a.json', name: '供应商记录整理', firstMessage: '帮我整理供应商', created: '', modified: new Date().toISOString(), messageCount: 12 }, - { id: 's2', path: 'b.json', name: '', firstMessage: '根据热点词起草一个商品标题方案', created: '', modified: new Date(Date.now() - 86400000).toISOString(), messageCount: 5 }, - { id: 's3', path: 'c.json', name: '', firstMessage: '查询最近一次上架任务的执行结果', created: '', modified: new Date(Date.now() - 3 * 86400000).toISOString(), messageCount: 8 } -] - -const CODE_SNIPPET = `export async function churnBatch() { - const flavor = await getFlavor("pistachio"); - const base = await dairy.fetch({ flavor }); - await freezer.store(base, { temp: "-16C" }); - if (!base.approved) return null; - return base.gallons; -}` - -const DIFF_SNIPPET = `@@ -1,4 +1,5 @@ - export const hero = "mint-chip"; --const windows = slots; -+const windows = slots.filter( -+ (s) => s.temp <= -12, -+); - schedule(windows);` - -const PARA = - '百香果风味在整个周末保持热销榜首。周六一早就要开始搅拌,让批次在下午高峰前有足够时间固化。同时需要预留两个晚间冷冻窗口,避免与常规生产冲突。' - -function PreviewCard({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ) -} - -function App() { - const [searchOpen, setSearchOpen] = useState(false) - const [applied, setApplied] = useState('') - const selectionBoxRef = useRef(null) - - // 调试分区:URL hash 指定只渲染某一节(#thinking / #streaming / #tools / - // #approval / #code / #selection / #loading / #sidebar / #none),空 hash 全渲染 - const only = window.location.hash.slice(1) - const show = (id: string): boolean => only === '' || only === id - - return ( -
-

beautifului.dev · 8 组件直连验收(light)

- -
-
- {show('thinking') && ( - - -
- -
-
- )} - - {show('streaming') && ( - - setApplied(q)}> - - - - )} - - {show('tools') && ( - -
- -
-
- )} - - {show('approval') && ( - - setApplied(approved ? '已允许' : '已拒绝')} /> -
- - )} - - {show('code') && ( - - -
- -
-
- )} - - {show('selection') && ( - -
-

{PARA}

- setApplied(`${instruction}:${selection.slice(0, 18)}…`)} /> -
- {applied &&

onApply → {applied}

} -
- )} - - {show('loading') && ( - - - - )} -
- - {show('sidebar') && ( -
- -
- undefined} - onOpenWorkspace={() => undefined} - onNewSession={() => setApplied('新会话')} - onOpenSession={() => setApplied('打开会话')} - onRenameSession={() => setApplied('重命名')} - onDeleteSession={() => setApplied('删除')} - onRefreshSessions={() => setApplied('刷新')} - onOpenSearch={() => setSearchOpen(true)} - onOpenSettings={() => setApplied('设置')} - /> -
-
-
- )} -
- - {show('sidebar') && ( - setSearchOpen(false)} - sessions={sessions} - onNewSession={() => setApplied('新会话')} - onRefreshSessions={() => setApplied('刷新')} - onOpenSettings={() => setApplied('设置')} - onOpenSession={() => setApplied('打开会话')} - /> - )} -
- ) -} - -createRoot(document.getElementById('root')!).render( - - - -) diff --git a/apps/desktop/preview/preview.css b/apps/desktop/preview/preview.css deleted file mode 100644 index 815d52b..0000000 --- a/apps/desktop/preview/preview.css +++ /dev/null @@ -1,6 +0,0 @@ -/* 预览页专用入口:复用应用全套样式(tailwind + beautifului foundation + 字体), - * 并显式声明组件源码目录参与工具类扫描(vite root 在 preview/ 下, - * 自动检测覆盖不到 ../src)。 */ -@import "../src/renderer/src/styles/globals.css"; - -@source "../src/renderer/src"; diff --git a/apps/desktop/scripts/capture-app.cjs b/apps/desktop/scripts/capture-app.cjs deleted file mode 100644 index 9c345ed..0000000 --- a/apps/desktop/scripts/capture-app.cjs +++ /dev/null @@ -1,263 +0,0 @@ -// 真实 App 渲染层截图(离屏,注入假 IPC 数据): -// npx electron scripts/capture-app.cjs -// 产出 preview-shots/app-*.png:展开态 / 收起态 / markdown 渲染 -const { app, BrowserWindow, ipcMain } = require('electron') -const { mkdirSync, writeFileSync, existsSync } = require('node:fs') -const { join } = require('node:path') - -app.commandLine.appendSwitch('disable-gpu') - -const ROOT = join(__dirname, '..') -const OUT = join(ROOT, 'preview-shots') - -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) - -const MARKDOWN_SAMPLE = [ - '## 1. "这是演示数据,仅用于验证链路"', - '', - '我配置的 `hotword_lookup` 工具目前连的是**演示实现(demo)**,[项目主页](https://example.com)。', - '', - '| 说明 | 结论 |', - '| :--- | ---: |', - '| **工具可用** | 工具这个"壳"是好的 —— 链路没有问题 |', - '| **数据源是空的** | 它背后的"仓库"是空的,查询返回"无匹配项" |', - '', - '- 列表项一,包含 `inline code`', - '- 列表项二,**加粗** 与 ~~删除线~~', - '', - '```python', - "print('Hello llm-ui!')", - 'for i in range(3):', - ' print(i)', - '```', - '', - '```rust', - 'fn main() {', - ' println!("Hello, World!");', - '}', - '```' -].join('\n') - -const FAKE_STATE = { - sessionId: 's1', - sessionName: 'hi', - streaming: false, - activeTools: [], - messageCount: 2, - workspace: 'C:\\fake\\workspace' -} - -const FAKE_SESSIONS = [ - { id: 's1', path: 'C:\\fake\\s1.jsonl', name: 'hi', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 2 }, - { id: 's2', path: 'C:\\fake\\s2.jsonl', name: 'hello world', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 1 } -] - -function registerFakeIpc() { - ipcMain.handle('agent:state', () => FAKE_STATE) - ipcMain.handle('sessions:list', () => FAKE_SESSIONS) - ipcMain.handle('workspaces:list', () => [{ name: 'Default', path: 'C:\\fake\\workspace' }]) - ipcMain.handle('models:catalog', () => ({ providers: [] })) - ipcMain.handle('settings:get', () => ({ systemPrompt: '' })) - ipcMain.handle('app:versions', () => ({ app: 'dev', electron: 'x', node: 'x', chrome: 'x', user: 'x' })) - ipcMain.handle('agent:send', () => undefined) - ipcMain.handle('chat:resources', () => ({ - commands: [{ name: 'daily-report', description: '生成每日经营日报', kind: 'prompt' }], - skills: [{ name: 'hotword-analysis', description: '分析近 7 天类目热点词' }] - })) - ipcMain.handle('files:list', () => [ - { path: 'data/products.csv', name: 'products.csv', isDir: false }, - { path: 'docs/sop.md', name: 'sop.md', isDir: false }, - { path: 'hello_world.py', name: 'hello_world.py', isDir: false }, - { path: 'assets', name: 'assets', isDir: true } - ]) -} - -async function capture(win, name) { - // 离屏窗口 capturePage 常给旧帧:连拍多张,取最后一张(大概率已提交新帧) - let image = null - for (let k = 0; k < 4; k += 1) { - win.webContents.invalidate() - await sleep(320) - image = await win.webContents.capturePage() - } - writeFileSync(join(OUT, name), image.toPNG()) - console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`) -} - -async function run() { - mkdirSync(OUT, { recursive: true }) - registerFakeIpc() - - const preload = join(ROOT, 'out', 'preload', 'index.cjs') - const win = new BrowserWindow({ - width: 1280, - height: 900, - show: false, - webPreferences: { - paintWhenInitiallyHidden: true, - backgroundThrottling: false, - preload: existsSync(preload) ? preload : undefined, - contextIsolation: true - } - }) - await win.loadFile(join(ROOT, 'out', 'renderer', 'index.html')) - // 清掉历史 localStorage(收起态持久化),再重载让侧栏以展开态启动 - await win.webContents.executeJavaScript('try { localStorage.clear() } catch {}') - await win.reload() - await sleep(1200) - - // 注入一条含 markdown 的会话(user + assistant) - win.webContents.send('agent:event', { - type: 'session_switched', - state: FAKE_STATE, - messages: [ - { role: 'user', content: '解释一下那两句话的意思' }, - { role: 'assistant', content: [{ type: 'text', text: MARKDOWN_SAMPLE }] } - ], - activeTools: {} - }) - await sleep(1200) - console.log( - 'hl-diag:', - await win.webContents.executeJavaScript( - `(() => { const codes = [...document.querySelectorAll('code')]; return JSON.stringify({ ready: window.__shikiReady, codes: codes.map((c) => c.querySelectorAll('span[style]').length) }) })()` - ) - ) - await capture(win, 'app-markdown.png') - - // 注入排队消息场景(streaming + 队列两条) - win.webContents.send('agent:event', { type: 'agent_start' }) - win.webContents.send('agent:event', { - type: 'queue_update', - steering: ['设置里面做成这样,分成基础设置、Agent 能力'], - followUp: ['随后帮我补上skills、命令和附件的支持,chatbox里面/调起命令或skill,@则是引用文件,加号是附件'] - }) - await sleep(900) - win.webContents.invalidate() - await sleep(300) - FAKE_STATE.permissionMode = 'confirm' - await capture(win, 'app-queue.png') - - // 权限模式菜单 - await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) - win.webContents.send('agent:event', { type: 'agent_end' }) - win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] }) - await sleep(500) - await win.webContents.executeJavaScript( - `[...document.querySelectorAll('button')].find((b) => b.title.startsWith('权限模式'))?.click()` - ) - await sleep(500) - win.webContents.invalidate() - await sleep(300) - console.log( - 'perm-diag:', - await win.webContents.executeJavaScript( - `(() => { const panels = [...document.querySelectorAll('div.fixed.z-40')]; const m = panels.find((d) => d.textContent.includes('减少确认次数')); if (!m) return 'closed'; const r = m.getBoundingClientRect(); const cs = getComputedStyle(m); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, opacity: cs.opacity, display: cs.display, zIndex: cs.zIndex, buttons: m.querySelectorAll('button').length }) })()` - ) - ) - await capture(win, 'app-permissions.png') - // 结束流式,恢复常规状态 - win.webContents.send('agent:event', { type: 'agent_end' }) - win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] }) - await sleep(400) - - // "/" 命令/技能面板 - const typeInPrompt = (text) => - win.webContents.executeJavaScript( - `(() => { const ta = document.querySelector('textarea[aria-label="Prompt"]'); if (!ta) return 'no-ta'; const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; set.call(ta, ${JSON.stringify(text)}); ta.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()` - ) - await typeInPrompt('/') - await sleep(600) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-slash-menu.png') - // "@ " 文件引用 - await typeInPrompt('帮我看看 @') - await sleep(600) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-at-menu.png') - await typeInPrompt('') - - // 注入审批卡 - win.webContents.send('agent:event', { - type: 'approval_request', - request: { - id: 'approval-1', - toolName: 'write', - args: { path: 'hello_world.py', content: 'def main():\n print("Hello world")\n\nmain()' } - } - }) - await sleep(800) - win.webContents.invalidate() - await sleep(400) - await capture(win, 'app-approval.png') - - // 行内操作浮层:悬停操作按钮点击后弹出(重命名 / 删除) - await win.webContents.executeJavaScript( - `[...document.querySelectorAll('button')].find((b) => b.title === '重命名会话')?.click()` - ) - await sleep(500) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-row-rename.png') - await win.webContents.executeJavaScript( - `[...document.querySelectorAll('button')].find((b) => b.title === '删除会话')?.click()` - ) - await sleep(500) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-row-delete.png') - await win.webContents.executeJavaScript(`document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))`) - - // 设置页:Ctrl+, 打开,截「模型设置」与「常规」 - await win.webContents.executeJavaScript( - `window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true, bubbles: true }))` - ) - await sleep(600) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-settings-model.png') - await win.webContents.executeJavaScript( - `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '常规')?.click()` - ) - await sleep(400) - win.webContents.invalidate() - await sleep(300) - await capture(win, 'app-settings-general.png') - await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) - - console.log( - 'diag:', - await win.webContents.executeJavaScript( - `(() => { const btn = document.querySelector('button[aria-label="拒绝并关闭"]'); if (!btn) return 'missing'; const card = btn.closest('div[class*="rounded-card"]') ?? btn.parentElement; const r = card.getBoundingClientRect(); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, winH: innerHeight, opacity: getComputedStyle(card).opacity }) })()` - ) - ) - - // 侧栏收起回归检查 - const collapsed = await win.webContents.executeJavaScript( - `(() => { const b = document.querySelector('[aria-label="折叠侧栏"]'); if (!b) return 'no-button'; b.click(); return 'clicked' })()` - ) - console.log('collapse:', collapsed) - await sleep(800) - const geo = await win.webContents.executeJavaScript( - `(() => { - const aside = document.querySelector('aside[aria-label="工作区导航"]') - const row = document.querySelector('.sidebar-row') - const icon = row ? row.querySelector('span') : null - if (!aside || !row || !icon) return 'missing' - const a = aside.getBoundingClientRect() - const i = icon.getBoundingClientRect() - return JSON.stringify({ rail: Math.round(a.width), iconCenter: Math.round(i.left - a.left + i.width / 2) }) - })()` - ) - console.log('collapsed-geo:', geo) - await capture(win, 'app-collapsed.png') - - app.exit(0) -} - -app.whenReady().then(run).catch((err) => { - console.error(err) - app.exit(1) -}) diff --git a/apps/desktop/scripts/capture-preview.cjs b/apps/desktop/scripts/capture-preview.cjs deleted file mode 100644 index 2353970..0000000 --- a/apps/desktop/scripts/capture-preview.cjs +++ /dev/null @@ -1,95 +0,0 @@ -// 预览页视觉验收截图(Electron 离屏捕获): -// npx electron scripts/capture-preview.cjs -// 产出 preview-shots/*.png:整页 / 侧栏折叠 / 搜索面板 / 选区工具条 -const { app, BrowserWindow } = require('electron') -const { mkdirSync, writeFileSync } = require('node:fs') -const { join } = require('node:path') - -// 离屏捕获在 GPU 合成异常时会得到空白帧,改用软件合成 -app.commandLine.appendSwitch('disable-gpu') - -const URL = 'http://localhost:4173/' -const OUT = join(__dirname, '..', 'preview-shots') - -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) - -async function capture(win, name) { - try { - const image = await win.webContents.capturePage() - writeFileSync(join(OUT, name), image.toPNG()) - console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`) - } catch (e) { - console.log('capture failed', name, e.message) - } -} - -async function js(win, code) { - try { - return await win.webContents.executeJavaScript(code) - } catch (e) { - console.log('js failed:', e.message) - return undefined - } -} - -async function run() { - mkdirSync(OUT, { recursive: true }) - const win = new BrowserWindow({ - width: 1280, - height: 960, - show: false, - webPreferences: { paintWhenInitiallyHidden: true } - }) - await win.loadURL(URL) - await sleep(1500) - - // 1) 整页(上半) - await capture(win, '01-full.png') - - // 2) 折叠侧栏(等宽度过渡) - await js(win, `document.querySelector('[aria-label="折叠侧栏"]')?.click()`) - await sleep(700) - await capture(win, '02-sidebar-collapsed.png') - await js(win, `document.querySelector('[aria-label="展开侧栏"]')?.click()`) - await sleep(700) - - // 3) 搜索面板(侧栏「搜索」行)+ 输入过滤 - await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '搜索')?.click()`) - await sleep(600) - await js(win, `(() => { const i = document.querySelector('input[aria-label="搜索会话或命令"]'); if (!i) return 'no-input'; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; set.call(i, '供应'); i.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()`) - await sleep(400) - await capture(win, '03-search-filtered.png') - await js(win, `document.querySelector('input[aria-label="搜索会话或命令"]')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`) - await sleep(400) - - // 4) 划选文本 → Selection Actions 工具条(展开「更多」) - await js(win, `(() => { - const p = [...document.querySelectorAll('p')].find((el) => el.textContent.includes('百香果')); - if (!p) return 'no-p'; - const textNode = p.firstChild; - const range = document.createRange(); - range.setStart(textNode, 0); - range.setEnd(textNode, 30); - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - return 'ok'; - })()`) - await sleep(600) - await js(win, `document.querySelector('[aria-label="更多操作"]')?.click()`) - await sleep(500) - await capture(win, '04-selection-actions.png') - - // 5) 审批卡交互:选择「允许执行」(radio 自动前进 480ms 后提交) - await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '允许执行')?.click()`) - await sleep(900) - await capture(win, '05-approval-approved.png') - - app.exit(0) -} - -app.whenReady().then(run).catch((err) => { - console.error(err) - app.exit(1) -}) diff --git a/apps/desktop/scripts/debug-collapse.cjs b/apps/desktop/scripts/debug-collapse.cjs deleted file mode 100644 index bfdb5b8..0000000 --- a/apps/desktop/scripts/debug-collapse.cjs +++ /dev/null @@ -1,21 +0,0 @@ -// 验证侧栏折叠交互 -const { app, BrowserWindow } = require('electron') -app.commandLine.appendSwitch('disable-gpu') -app.commandLine.appendSwitch('disable-software-rasterizer') - -app.whenReady().then(async () => { - const win = new BrowserWindow({ width: 1280, height: 900, show: false, webPreferences: { offscreen: true } }) - await win.loadURL('http://127.0.0.1:4173/') - await new Promise((r) => setTimeout(r, 1200)) - const found = await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]') ? 'found' : 'NOT-FOUND'`) - console.log('collapse btn:', found) - await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]').click()`) - await new Promise((r) => setTimeout(r, 600)) - console.log( - 'after click: collapsed=', - await win.webContents.executeJavaScript(`document.querySelector('aside')?.getAttribute('data-sidebar-collapsed')`), - 'width=', - await win.webContents.executeJavaScript(`document.querySelector('aside')?.style.width`) - ) - app.exit(0) -}) diff --git a/apps/desktop/scripts/debug-preview.cjs b/apps/desktop/scripts/debug-preview.cjs deleted file mode 100644 index 6d8d806..0000000 --- a/apps/desktop/scripts/debug-preview.cjs +++ /dev/null @@ -1,41 +0,0 @@ -// 预览页控制台诊断:node 调试某一分区 -// npx electron scripts/debug-preview.cjs "#thinking" -const { app, BrowserWindow } = require('electron') - -const hash = process.argv[2] ?? '' -const URL = `http://127.0.0.1:4173/${hash}` - -// 兜底退出(渲染进程卡死时 executeJavaScript 永不返回) -const guard = setTimeout(() => { - console.log('[guard] hard exit (page busy or crashed)') - app.exit(2) -}, 12000) - -app.whenReady().then(async () => { - const win = new BrowserWindow({ - width: 1280, - height: 900, - show: false, - webPreferences: { offscreen: true } - }) - win.webContents.on('console-message', (_e, level, message, line, source) => { - console.log(`[console:${level}]`, message, `(${source}:${line})`) - }) - win.webContents.on('did-fail-load', (_e, code, desc, url) => { - console.log(`[did-fail-load] ${code} ${desc} ${url}`) - }) - win.webContents.on('render-process-gone', (_e, details) => { - console.log('[crashed]', details.reason) - }) - await win.loadURL(URL) - setTimeout(async () => { - try { - const text = await win.webContents.executeJavaScript('document.body.innerText.slice(0, 120)') - console.log('[body]', JSON.stringify(text)) - } catch (e) { - console.log('[executeJavaScript failed]', e.message) - } - clearTimeout(guard) - app.exit(0) - }, 2500) -}) diff --git a/apps/desktop/scripts/inspect-bundle.cjs b/apps/desktop/scripts/inspect-bundle.cjs deleted file mode 100644 index f281903..0000000 --- a/apps/desktop/scripts/inspect-bundle.cjs +++ /dev/null @@ -1,17 +0,0 @@ -// 对比 out/renderer 旧 bundle 与当前源码的 reducer 逻辑 -const fs = require('fs') -const t = fs.readFileSync('apps/desktop/out/renderer/assets/index-DJXyfP9T.js', 'utf8') - -console.log('has message_end:', t.includes('message_end')) -console.log('has optimistic-user:', t.includes('optimistic-user')) -console.log('has session_switched:', t.includes('session_switched')) -console.log('has agent_settled:', t.includes('agent_settled')) -console.log('has approval_request:', t.includes('approval_request')) - -// reducer 里 message_end 附近的代码 -let i = t.indexOf('"message_end"') -if (i < 0) i = t.indexOf('message_end') -if (i >= 0) { - console.log('\n=== around first message_end ===') - console.log(t.slice(i - 500, i + 900)) -} diff --git a/apps/desktop/scripts/make-icons.cjs b/apps/desktop/scripts/make-icons.cjs deleted file mode 100644 index 2122495..0000000 --- a/apps/desktop/scripts/make-icons.cjs +++ /dev/null @@ -1,87 +0,0 @@ -// 生成应用图标(build/icon.ico + build/icon.png): -// npx electron scripts/make-icons.cjs -// 用 Electron 离屏渲染 build/icon.svg(居中于方形画布、留 9% 边距), -// 输出多尺寸 PNG 并打包为 PNG 内嵌式 ICO(Vista+ 支持)。 -const { app, BrowserWindow } = require('electron') -const { readFileSync, writeFileSync } = require('node:fs') -const { join } = require('node:path') - -app.commandLine.appendSwitch('disable-gpu') - -const SIZES = [16, 24, 32, 48, 64, 128, 256] -const ROOT = join(__dirname, '..') - -const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) - -/** PNG 内嵌式 ICO 打包 */ -function buildIco(pngs) { - const count = pngs.length - const header = Buffer.alloc(6) - header.writeUInt16LE(0, 0) // reserved - header.writeUInt16LE(1, 2) // type: icon - header.writeUInt16LE(count, 4) - const entries = Buffer.alloc(16 * count) - const blobs = [] - let offset = 6 + 16 * count - pngs.forEach((png, i) => { - const size = png.size - entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 0) // width - entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 1) // height - entries.writeUInt8(0, i * 16 + 2) // palette - entries.writeUInt8(0, i * 16 + 3) // reserved - entries.writeUInt16LE(1, i * 16 + 4) // planes - entries.writeUInt16LE(32, i * 16 + 6) // bpp - entries.writeUInt32LE(png.data.length, i * 16 + 8) - entries.writeUInt32LE(offset, i * 16 + 12) - offset += png.data.length - blobs.push(png.data) - }) - return Buffer.concat([header, entries, ...blobs]) -} - -async function renderSize(win, svgDataUrl, size) { - await win.loadURL( - 'data:text/html,' + - encodeURIComponent( - `` + - `
` + - `` + - `
` - ) - ) - await sleep(350) - const image = await win.webContents.capturePage({ x: 0, y: 0, width: size, height: size }) - return image.toPNG() -} - -async function run() { - const svg = readFileSync(join(ROOT, 'build', 'icon.svg'), 'utf8') - const svgDataUrl = 'data:image/svg+xml;base64,' + Buffer.from(svg, 'utf8').toString('base64') - - const win = new BrowserWindow({ - width: 256, - height: 256, - show: false, - frame: false, - transparent: true, - webPreferences: { paintWhenInitiallyHidden: true, offscreen: true } - }) - - const pngs = [] - for (const size of SIZES) { - await win.setSize ? win.setSize(size, size) : null - const png = await renderSize(win, svgDataUrl, size) - pngs.push({ size, data: png }) - console.log('rendered', size, `${png.length}B`) - } - - writeFileSync(join(ROOT, 'build', 'icon.png'), pngs.find((p) => p.size === 256).data) - writeFileSync(join(ROOT, 'build', 'icon.ico'), buildIco(pngs)) - console.log('written build/icon.ico & build/icon.png') - app.exit(0) -} - -app.whenReady().then(run).catch((err) => { - console.error(err) - app.exit(1) -}) diff --git a/apps/desktop/scripts/read-dts.cjs b/apps/desktop/scripts/read-dts.cjs deleted file mode 100644 index 535f5ab..0000000 --- a/apps/desktop/scripts/read-dts.cjs +++ /dev/null @@ -1,15 +0,0 @@ -// 打印 pi SDK 关键类型定义(skills / slash-commands / prompt 参数) -const fs = require('node:fs') -const files = [ - 'dist/core/skills.d.ts', - 'dist/core/slash-commands.d.ts', - 'dist/modes/interactive/components/skill-invocation-message.d.ts' -] -for (const f of files) { - console.log('=====', f) - try { - console.log(fs.readFileSync(f, 'utf8')) - } catch (e) { - console.log('READ FAIL', e.message) - } -} diff --git a/apps/desktop/scripts/read-dts2.cjs b/apps/desktop/scripts/read-dts2.cjs deleted file mode 100644 index af57092..0000000 --- a/apps/desktop/scripts/read-dts2.cjs +++ /dev/null @@ -1,22 +0,0 @@ -// 打印 ImageContent / PromptTemplate / AgentSessionServices 类型 -const fs = require('node:fs') -const root = 'C:/Users/21976/Desktop/Develop/agent-store-operations/apps/desktop/node_modules' -const files = [ - [root + '/@earendil-works/pi-ai/dist/compat.d.ts', /export (interface|type) (ImageContent|TextContent)[\s\S]*?\n\}/], - [root + '/@earendil-works/pi-coding-agent/dist/core/prompt-templates.d.ts', null], - [root + '/@earendil-works/pi-coding-agent/dist/core/sdk.d.ts', /export interface AgentSessionServices[\s\S]*?\n\}/] -] -for (const [f, re] of files) { - console.log('=====', f.split('/').slice(-2).join('/')) - try { - const s = fs.readFileSync(f, 'utf8') - if (re) { - const m = s.match(re) - console.log(m ? m[0] : '(no match)') - } else { - console.log(s.slice(0, 2500)) - } - } catch (e) { - console.log('READ FAIL', e.message) - } -} diff --git a/apps/desktop/scripts/repro-empty-workspace.mjs b/apps/desktop/scripts/repro-empty-workspace.mjs deleted file mode 100644 index 6aabd83..0000000 --- a/apps/desktop/scripts/repro-empty-workspace.mjs +++ /dev/null @@ -1,21 +0,0 @@ -// 验证 pi SDK 对空目录(无会话的工作区)的行为: -// node scripts/repro-empty-workspace.mjs "C:\path\to\empty\dir" -import { SessionManager, getAgentDir } from '@earendil-works/pi-coding-agent' -import { mkdirSync, mkdtempSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' - -const target = process.argv[2] ?? mkdtempSync(join(tmpdir(), 'ws-empty-')) -mkdirSync(target, { recursive: true }) -console.log('target cwd:', target) - -try { - const sm = SessionManager.continueRecent(target) - console.log('continueRecent OK, sessionFile:', sm.sessionFile ?? '(none)') - const list = await SessionManager.list(target) - console.log('list count:', list.length) -} catch (err) { - console.error('continueRecent FAILED:', err instanceof Error ? err.message : err) - process.exit(1) -} -console.log('agentDir:', getAgentDir()) diff --git a/apps/desktop/scripts/repro-event-flow.mjs b/apps/desktop/scripts/repro-event-flow.mjs deleted file mode 100644 index fc3a8b3..0000000 --- a/apps/desktop/scripts/repro-event-flow.mjs +++ /dev/null @@ -1,103 +0,0 @@ -// 复现渲染层"输出一闪而过":用与 agent-service.ts 相同的 SDK 调用链, -// 订阅全部事件并打印事件序列与消息对象结构。 -import os from 'node:os' -import { - createAgentSessionRuntime, - createAgentSessionServices, - createAgentSessionFromServices, - getAgentDir, - SessionManager -} from '@earendil-works/pi-coding-agent' - -const cwd = os.homedir() -const agentDir = getAgentDir() - -const services = await createAgentSessionServices({ cwd, agentDir }) -const factory = async (opts) => { - const result = await createAgentSessionFromServices({ - services, - sessionManager: opts.sessionManager, - sessionStartEvent: opts.sessionStartEvent - }) - return { ...result, services, diagnostics: services.diagnostics } -} - -const sessionManager = SessionManager.continueRecent(cwd) -const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager }) -const session = runtime.session - -let rebindCount = 0 -runtime.setRebindSession(async (s) => { - rebindCount++ - console.log(`\n>>> REBIND fired (#${rebindCount}) — 渲染层会收到 session_switched,messages.length=${s.messages.length}`) -}) - -session.subscribe((event) => { - switch (event.type) { - case 'message_start': - console.log('EVT message_start role=', JSON.stringify(event.message?.role)) - break - case 'message_update': { - const inner = event.assistantMessageEvent - if (inner) { - if (inner.type.endsWith('_delta')) { - process.stdout.write(`.`) - } else { - console.log(`EVT message_update ${inner.type}`) - } - } - break - } - case 'message_end': { - const m = event.message - console.log('\nEVT message_end role=', m?.role) - console.log(' message keys:', Object.keys(m ?? {})) - try { - JSON.stringify(m) - console.log(' JSON.stringify: OK, length=', JSON.stringify(m).length) - } catch (err) { - console.log(' JSON.stringify FAILED ->', err.message, '(toCloneable 会变成 null → 渲染层消息变空!)') - } - if (m?.role === 'assistant') { - console.log(' stopReason=', m.stopReason, ' errorMessage=', m.errorMessage) - console.log(' content types=', (m.content ?? []).map((b) => b.type).join(',')) - for (const b of m.content ?? []) { - if (b.type === 'text') console.log(' text block:', JSON.stringify(b.text).slice(0, 200)) - } - } - break - } - case 'agent_end': - console.log(`\nEVT agent_end willRetry=${event.willRetry} messages=${event.messages?.length}`) - break - default: - console.log('EVT', event.type) - } -}) - -console.log('--- session:', session.sessionId, 'messages:', session.messages.length) -console.log('--- model:', session.model?.provider + '/' + session.model?.id) -console.log('--- sending "hi" ...') - -try { - await session.prompt('hi') - console.log('\n--- prompt resolved') -} catch (err) { - console.log('\n--- prompt THREW:', err?.message) -} - -// 等待 agent_settled 之后的异步收尾 -await new Promise((r) => setTimeout(r, 1500)) - -console.log('\n=== final session.messages ===') -for (const [i, m] of session.messages.entries()) { - const blocks = Array.isArray(m.content) - ? m.content.map((b) => b.type).join(',') - : typeof m.content === 'string' - ? 'str:' + m.content.slice(0, 30) - : '??' - console.log(`#${i} ${m.role} [${blocks}]`) -} - -console.log('\nrebindCount=', rebindCount) -process.exit(0) diff --git a/apps/desktop/scripts/repro-markdown.mjs b/apps/desktop/scripts/repro-markdown.mjs deleted file mode 100644 index d37c46a..0000000 --- a/apps/desktop/scripts/repro-markdown.mjs +++ /dev/null @@ -1,41 +0,0 @@ -// 验证 Markdown.tsx 的 parseBlocks 是否产出空内容 -const INLINE = /^(\s*)([-*+]\s+|\d+[.)]\s+|>\s?|(#{1,3}\s+))?(.*)$/ - -function parseBlocks(text) { - const blocks = [] - const lines = text.split('\n') - let current = null - const flush = () => { - if (current) blocks.push(current) - current = null - } - for (const line of lines) { - const m = line.match(INLINE) - if (!m) continue - const [, indent, marker, content] = m - const isListItem = Boolean(marker && /^([-*+]\s+|\d+[.)]\s+)/.test(marker)) - const kind = marker - ? marker.startsWith('#') - ? 'h1' - : isListItem - ? /^\d/.test(marker) ? 'ol' : 'ul' - : marker.startsWith('>') ? 'quote' : null - : 'p' - if (kind === 'p') { - if (!current || current.kind !== 'p') { - flush() - current = { kind: 'p', lines: [] } - } - current.lines.push(content) - continue - } - flush() - } - return blocks -} - -for (const t of ['pong', 'Hello world', '# 标题', '- 列表项', '> 引用', '1. 有序']) { - const blocks = parseBlocks(t) - const rendered = blocks.map((b) => b.lines.join('\n')) - console.log(JSON.stringify(t), '->', JSON.stringify(rendered)) -} diff --git a/apps/desktop/scripts/repro-render-logic.mjs b/apps/desktop/scripts/repro-render-logic.mjs deleted file mode 100644 index 63834d1..0000000 --- a/apps/desktop/scripts/repro-render-logic.mjs +++ /dev/null @@ -1,81 +0,0 @@ -// 验证渲染层对 session_switched 历史消息的渲染逻辑: -// 用与主进程一致的 session.messages 源,跑 MessageItem 的 readBlocks + AssistantBody 判断。 -import os from 'node:os' -import { createAgentSessionServices, getAgentDir } from '@earendil-works/pi-coding-agent' - -const cwd = os.homedir() -const services = await createAgentSessionServices({ cwd, agentDir: getAgentDir() }) - -// SessionManager.list + 加载最近会话的等价做法:直接用 services 里加载当前会话 -const session = services.sessionManager - ? null - : null - -// 走 SessionManager 拿最近会话并加载 -const { SessionManager } = await import('@earendil-works/pi-coding-agent') -const sm = SessionManager.continueRecent(cwd) -const smAny = sm -// SessionManager.continueRecent 返回的 manager 需要挂到 session 上才有 messages; -// 更直接:读它指向的会话文件并解析 message 条目 -const entries = [] -if (typeof smAny.loadEntries === 'function') { - entries.push(...(await smAny.loadEntries())) -} -console.log('loadEntries available:', typeof smAny.loadEntries === 'function', 'entries:', entries.length) - -// 退路:直接解析磁盘上的最近 jsonl(与主进程 toCloneable 后的消息同构) -import { readdirSync, readFileSync, statSync } from 'node:fs' -import { join } from 'node:path' -const dir = join(cwd, '.pi', 'agent', 'sessions', '--C--Users-21976--') -const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl')) -files.sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs) -const latest = files[0] -console.log('latest session file:', latest) - -const lines = readFileSync(join(dir, latest), 'utf8').trim().split('\n') -const messages = [] -for (const line of lines) { - let entry - try { - entry = JSON.parse(line) - } catch { - continue - } - if (entry.type === 'message' && entry.message) messages.push(entry.message) -} - -// ==== 渲染层逻辑复刻(与 MessageItem.tsx 一致)==== -function readBlocks(raw) { - if (!raw || typeof raw !== 'object') return [] - const content = raw.content - if (typeof content === 'string') return [{ type: 'text', text: content }] - if (Array.isArray(content)) { - return content.filter((c) => typeof c === 'object' && c !== null) - } - return [] -} - -for (const [i, m] of messages.entries()) { - console.log(`\n=== history msg #${i} role=${m.role} ===`) - const blocks = readBlocks(m) - let rendered = 0 - for (const block of blocks) { - if (block.type === 'thinking' && typeof block.text === 'string') { - console.log(' -> ThinkingBlock (text field)') - rendered++ - continue - } - if (block.type === 'text' && block.text) { - console.log(` -> Markdown("${String(block.text).slice(0, 60)}…")`) - rendered++ - continue - } - if (block.type === 'toolCall' && block.id) { - console.log(' -> ToolCard', block.name) - rendered++ - continue - } - console.log(' -> **被丢弃** block type =', block.type, 'keys =', Object.keys(block).join(',')) - } - console.log(` rendered ${rendered}/${blocks.length} blocks`) -} diff --git a/apps/desktop/scripts/smoke-models.mjs b/apps/desktop/scripts/smoke-models.mjs deleted file mode 100644 index 3466310..0000000 --- a/apps/desktop/scripts/smoke-models.mjs +++ /dev/null @@ -1,65 +0,0 @@ -/** - * v0.3.0 冒烟:模型目录 / API key login-logout / setModel / 系统提示词 - * 不打真实 LLM 请求;key 用假值(仅验证凭据写入与模型切换逻辑)。 - */ -import { createAgentSessionServices, createAgentSessionRuntime, createAgentSessionFromServices, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent' -import { homedir } from 'node:os' -import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs' -import { join } from 'node:path' -import { tmpdir } from 'node:os' - -const cwd = homedir() -const agentDir = getAgentDir() - -const services = await createAgentSessionServices({ cwd, agentDir }) -const factory = async ({ cwd: nc, agentDir: na, sessionManager }) => { - const svc = nc === services.cwd && na === services.agentDir ? services : await createAgentSessionServices({ cwd: nc, agentDir: na }) - const result = await createAgentSessionFromServices({ services: svc, sessionManager }) - return { ...result, services: svc, diagnostics: svc.diagnostics } -} -const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager: SessionManager.continueRecent(cwd) }) -const mr = runtime.session.modelRuntime - -// 1) 模型目录 -const providers = mr.getProviders() -console.log('[smoke3] providers:', providers.map((p) => p.id).join(', ')) -const anthropic = providers.find((p) => p.id === 'anthropic') -if (anthropic) { - const models = mr.getModels('anthropic') - console.log('[smoke3] anthropic models: %d (first: %s)', models.length, models[0]?.id) - - // 2) login(假 key)→ listCredentials - const before = await mr.listCredentials() - await mr.login('anthropic', 'api_key', { prompt: async () => 'sk-smoke-fake-key', notify: async () => undefined }) - const after = await mr.listCredentials() - console.log('[smoke3] credentials before=%d after=%d (anthropic=%s)', before.length, after.length, after.some((c) => c.providerId === 'anthropic' && c.type === 'api_key')) - - // auth.json 应包含 anthropic 条目 - const auth = JSON.parse(readFileSync(join(agentDir, 'auth.json'), 'utf8')) - console.log('[smoke3] auth.json has anthropic:', Boolean(auth.anthropic)) - - // 3) setModel(有凭据时允许) - const target = models[0] - await runtime.session.setModel(target, { persist: false }) - console.log('[smoke3] setModel ok -> %s/%s', runtime.session.model?.provider, runtime.session.model?.id) - - // 4) logout - await mr.logout('anthropic') - const finalCreds = await mr.listCredentials() - console.log('[smoke3] after logout, anthropic credential present:', finalCreds.some((c) => c.providerId === 'anthropic')) -} else { - console.log('[smoke3] anthropic not found in providers, skip key tests') -} - -// 5) 系统提示词 override(独立 services,用 resourceLoaderOptions.systemPrompt) -const tmp = mkdtempSync(join(tmpdir(), 'aso-smoke-')) -const promptText = '你是冒烟测试助手。' -const svc2 = await createAgentSessionServices({ cwd, agentDir, resourceLoaderOptions: { systemPrompt: promptText } }) -console.log('[smoke3] systemPrompt applied:', svc2.resourceLoader ? 'services-created' : 'fail') -const rt2 = await createAgentSessionFromServices({ services: svc2, sessionManager: SessionManager.inMemory(cwd) }) -console.log('[smoke3] session systemPrompt startsWith expected:', rt2.session.systemPrompt.startsWith(promptText.slice(0, 8))) - -await runtime.dispose() -writeFileSync(join(tmp, 'done'), '') -console.log('[smoke3] done') -process.exit(0) diff --git a/apps/desktop/scripts/smoke-sessions.mjs b/apps/desktop/scripts/smoke-sessions.mjs deleted file mode 100644 index ad353ad..0000000 --- a/apps/desktop/scripts/smoke-sessions.mjs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * pi SDK 会话管理冒烟测试(v0.2.0) - * 验证:runtime 创建、list、newSession、switchSession(历史恢复)、rename、删除(回收站)。 - */ -import { createAgentSessionRuntime, createAgentSessionServices, createAgentSessionFromServices, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent' -import { homedir } from 'node:os' -import { join } from 'node:path' - -const cwd = homedir() -const agentDir = getAgentDir() - -console.log('[smoke2] listing sessions (cwd=%s)...', cwd) -const before = await SessionManager.list(cwd) -console.log('[smoke2] found %d sessions', before.length) - -const initialServices = await createAgentSessionServices({ cwd, agentDir }) -const factory = async ({ cwd: nextCwd, agentDir: nextAgentDir, sessionManager, sessionStartEvent }) => { - const services = - nextCwd === initialServices.cwd && nextAgentDir === initialServices.agentDir - ? initialServices - : await createAgentSessionServices({ cwd: nextCwd, agentDir: nextAgentDir }) - const result = await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent }) - return { ...result, services, diagnostics: services.diagnostics } -} - -const sessionManager = SessionManager.create(cwd) -const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager }) -let rebindCount = 0 -runtime.setRebindSession(async () => { - rebindCount += 1 -}) -console.log('[smoke2] runtime ready, session=%s', runtime.session.sessionId) - -// 重命名 -runtime.session.setSessionName('smoke-测试会话') -console.log('[smoke2] renamed to: %s', runtime.session.sessionName) - -// 会话里追加用户+助手消息(直接写会话文件,不触发 LLM) -// pi 约定:只有出现 assistant 消息后才会真正落盘(避免空会话残留文件) -runtime.session.sessionManager.appendMessage({ role: 'user', content: '这是一条冒烟测试消息', timestamp: Date.now() }) -runtime.session.sessionManager.appendMessage({ - role: 'assistant', - content: [{ type: 'text', text: '冒烟回复(不经过 LLM)' }], - api: 'openai-completions', - provider: 'smoke', - model: 'smoke-model', - usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 }, - stopReason: 'stop', - timestamp: Date.now() -}) -console.log('[smoke2] messages in session:', runtime.session.messages.length) - -const listAfter = await SessionManager.list(cwd) -const mine = listAfter.find((s) => s.name === 'smoke-测试会话' || s.firstMessage.includes('冒烟测试')) -console.log('[smoke2] session visible in list: %s (total %d)', Boolean(mine), listAfter.length) -if (mine) { - console.log('[smoke2] meta: name=%s firstMessage=%s count=%d', mine.name, JSON.stringify(mine.firstMessage.slice(0, 30)), mine.messageCount) - - // 新会话(切换) - await runtime.newSession() - console.log('[smoke2] newSession ok, rebind=%d, new id=%s, messages=%d', rebindCount, runtime.session.sessionId, runtime.session.messages.length) - - // 切回 - await runtime.switchSession(mine.path) - console.log( - '[smoke2] switch back ok, rebind=%d, id=%s, messages restored=%d, name=%s', - rebindCount, - runtime.session.sessionId, - runtime.session.messages.length, - runtime.session.sessionName ?? '(none)' - ) -} - -await runtime.dispose() -console.log('[smoke2] done') -process.exit(0) diff --git a/apps/desktop/scripts/smoke-tools.mjs b/apps/desktop/scripts/smoke-tools.mjs deleted file mode 100644 index 6915bd0..0000000 --- a/apps/desktop/scripts/smoke-tools.mjs +++ /dev/null @@ -1,66 +0,0 @@ -/** - * v0.4.0 冒烟:业务工具注册 / 手动直调(D2 双模式路径)/ tool_call 拦截契约 - * 说明:审批拦截逻辑与 src/main/pi/approval.ts 保持一致(此处内联同构实现, - * 因冒烟脚本无法直接加载 TS 源文件;拦截契约由 pi 的 ExtensionHandler 类型保证)。 - */ -import { createAgentSessionFromServices, createAgentSessionServices, defineTool, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent' -import { Type } from '@earendil-works/pi-ai' -import { homedir } from 'node:os' - -const cwd = homedir() -const agentDir = getAgentDir() - -// 与 src/main/tools/demo-tools.ts 同构的工具定义 -const hotwordLookup = defineTool({ - name: 'hotword_lookup', - label: '热点词查询(演示)', - description: 'demo', - parameters: Type.Object({ query: Type.String(), limit: Type.Optional(Type.Number()) }), - async execute(_id, params) { - return { - content: [{ type: 'text', text: `热点词查询结果(演示数据源,query="${params.query}"):ok` }], - details: { source: 'demo' } - } - } -}) - -// ---- 1) 工具注册检查(customTools 路径) ---- -const services = await createAgentSessionServices({ cwd, agentDir }) -const { session } = await createAgentSessionFromServices({ - services, - sessionManager: SessionManager.inMemory(cwd), - customTools: [hotwordLookup] -}) -const all = session.getAllTools() -console.log('[smoke4] registered tools:', all.map((t) => t.name).join(', ')) -console.log('[smoke4] hotword_lookup registered:', all.some((t) => t.name === 'hotword_lookup')) - -// ---- 2) 手动直调(双模式共用能力的证明,不经 LLM) ---- -const result = await hotwordLookup.execute('smoke-call', { query: '宠物' }, undefined, undefined, {}) -console.log('[smoke4] manual execute text:', JSON.stringify(result.content[0]?.text?.slice(0, 60))) - -// ---- 3) tool_call 拦截契约(模拟 ExtensionAPI 捕获 handler) ---- -const handlers = {} -const fakePi = { on: (event, handler) => { handlers[event] = handler } } -const needsApproval = new Set(['price_check', 'bash']) -let decision = false -fakePi.on('tool_call', async (event) => { - if (!needsApproval.has(event.toolName)) return - const approved = decision - if (!approved) return { block: true, reason: '用户拒绝了本次工具调用审批' } - return -}) - -const passthrough = await handlers['tool_call']({ type: 'tool_call', toolName: 'read', input: { path: 'x' } }) -console.log('[smoke4] read(不在名单)放行:', passthrough === undefined) - -decision = false -const denied = await handlers['tool_call']({ type: 'tool_call', toolName: 'price_check', input: { product_id: 'P1', target_price: 9.9 } }) -console.log('[smoke4] price_check 拒绝 -> block:', JSON.stringify(denied)) - -decision = true -const approvedRes = await handlers['tool_call']({ type: 'tool_call', toolName: 'bash', input: { command: 'ls' } }) -console.log('[smoke4] bash 批准 -> 放行:', approvedRes === undefined) - -console.log('[smoke4] done') -process.exit(0) diff --git a/apps/desktop/scripts/smoke.mjs b/apps/desktop/scripts/smoke.mjs deleted file mode 100644 index cb6442a..0000000 --- a/apps/desktop/scripts/smoke.mjs +++ /dev/null @@ -1,39 +0,0 @@ -/** - * pi SDK 集成冒烟测试(与 src/main/pi/agent-service.ts 相同的调用路径) - * 用法:node scripts/smoke.mjs - * 验证点:会话创建、事件订阅、prompt 在无凭据时的报错传播。 - */ -import { createAgentSession } from '@earendil-works/pi-coding-agent' -import { homedir } from 'node:os' - -const events = [] -let settled = false - -console.log('[smoke] creating session (cwd=%s)...', homedir()) -const { session } = await createAgentSession({ cwd: homedir() }) -console.log('[smoke] session created: id=%s model=%s/%s', session.sessionId, session.model?.provider ?? '-', session.model?.id ?? '-') - -session.subscribe((event) => { - events.push(event.type) - if (event.type === 'message_update' && event.assistantMessageEvent?.type?.endsWith('_delta')) { - const d = event.assistantMessageEvent.delta ?? '' - process.stdout.write(d) - } -}) - -try { - await session.prompt('回复"链路正常"两个字即可,不要调用任何工具。') - console.log('\n[smoke] prompt resolved; event sequence:', [...new Set(events)].join(' -> ')) - const last = session.getLastAssistantText() - console.log('[smoke] last assistant text:', last ? JSON.stringify(last.slice(0, 120)) : '(none)') - settled = true -} catch (err) { - console.log('\n[smoke] prompt rejected as expected (no credentials):', err instanceof Error ? err.message : err) -} - -const stats = session.getSessionStats() -console.log('[smoke] stats:', JSON.stringify({ messages: session.messages.length, cost: stats.cost })) -console.log('[smoke] active tools:', session.getActiveToolNames().join(', ')) -session.dispose() -console.log('[smoke] done, settled=%s', String(settled)) -process.exit(0) diff --git a/scripts/beautifului/approval-card.json b/scripts/beautifului/approval-card.json deleted file mode 100644 index cfda56c..0000000 --- a/scripts/beautifului/approval-card.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "approval-card", - "type": "registry:component", - "title": "Approval Card", - "description": "Human-in-the-loop questions the agent asks before acting.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json", - "https://www.beautifului.dev/r/button.json", - "https://www.beautifului.dev/r/glide-menu.json" - ], - "files": [ - { - "path": "components/primitives/ApprovalCard.tsx", - "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { Button } from \"@/components/atoms/Button\";\nimport GlideMenu from \"@/components/primitives/GlideMenu\";\n\n/* ─────────────────────────────────────────────────────────\n * APPROVAL CARD (human-in-the-loop)\n * One question at a time. The stack slides vertically as you\n * move between questions (the card's height animates to fit),\n * the step counter rolls like an odometer, and the footer uses\n * pill actions — a quiet Skip and a dark Continue with a ⏎.\n * Single-choice answers auto-advance; multi-select waits.\n * ───────────────────────────────────────────────────────── */\n\nexport type ApprovalQuestion = {\n q: string;\n type: \"radio\" | \"check\";\n options: string[];\n};\n\nconst QUESTIONS: ApprovalQuestion[] = [\n {\n q: \"How many flavors should we launch?\",\n type: \"radio\",\n options: [\"Three (core line)\", \"Five (full case)\", \"Just one hero\"],\n },\n {\n q: \"Which mix-ins should we stock?\",\n type: \"check\",\n options: [\"Chocolate chips\", \"Waffle bits\", \"Sprinkles\"],\n },\n {\n q: \"Which market do we enter first?\",\n type: \"radio\",\n options: [\"Food trucks\", \"Grocery freezers\", \"Scoop shops\"],\n },\n];\n\nexport type ApprovalLabels = {\n skip: string;\n continue: string;\n send: string;\n customPlaceholder: string;\n sentMessage: string;\n};\n\nconst DEFAULT_LABELS: ApprovalLabels = {\n skip: \"Skip\",\n continue: \"Continue\",\n send: \"Send\",\n customPlaceholder: \"Something else…\",\n sentMessage: \"Answers sent\",\n};\n\nconst ROLL_MS = 400;\nconst SLIDE = \"360ms cubic-bezier(0.22, 1, 0.36, 1)\";\n\n/* odometer digits — each character that changes rolls up (or down) */\nfunction RollingDigits({ value }: { value: string }) {\n const prevRef = useRef(value);\n const [oldVal, setOldVal] = useState(value);\n const [newVal, setNewVal] = useState(value);\n const [rolling, setRolling] = useState(false);\n const [shifted, setShifted] = useState(false);\n const [dir, setDir] = useState<\"up\" | \"down\">(\"up\");\n\n useEffect(() => {\n if (prevRef.current === value) return;\n const from = prevRef.current;\n prevRef.current = value;\n const fromN = parseInt(from, 10);\n const toN = parseInt(value, 10);\n setDir(Number.isFinite(fromN) && Number.isFinite(toN) && toN < fromN ? \"down\" : \"up\");\n setOldVal(from);\n setNewVal(value);\n setRolling(true);\n setShifted(false);\n\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(() => setShifted(true));\n });\n const done = setTimeout(() => {\n setRolling(false);\n setOldVal(value);\n setShifted(false);\n }, ROLL_MS);\n\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n clearTimeout(done);\n };\n }, [value]);\n\n const chars = rolling ? newVal : oldVal;\n\n return (\n <>\n {Array.from({ length: chars.length }, (_, i) => {\n const o = oldVal[i] ?? \"\";\n const n = chars[i] ?? \"\";\n if (!rolling || o === n) {\n return {n};\n }\n const top = dir === \"down\" ? n : o;\n const bottom = dir === \"down\" ? o : n;\n const restY = dir === \"down\" ? \"0\" : \"-1em\";\n const startY = dir === \"down\" ? \"-1em\" : \"0\";\n return (\n \n \n {top}\n {bottom}\n \n \n );\n })}\n \n );\n}\n\nfunction Ico({ path, size = 14, sw = 2 }: { path: React.ReactNode; size?: number; sw?: number }) {\n return (\n \n {path}\n \n );\n}\n\nexport default function ApprovalCard({\n questions = QUESTIONS,\n labels,\n onSubmitted,\n onAnswerChange,\n resettable = true,\n}: {\n questions?: ApprovalQuestion[];\n labels?: Partial;\n onSubmitted?: (answers: Record) => void;\n onAnswerChange?: (questionIndex: number, answer: number[]) => void;\n resettable?: boolean;\n variant?: string;\n} = {}) {\n const t = { ...DEFAULT_LABELS, ...labels };\n const [qi, setQi] = useState(0);\n const [answers, setAnswers] = useState>({});\n const [custom, setCustom] = useState>({});\n const [sent, setSent] = useState(false);\n const [open, setOpen] = useState(true);\n\n const advanceTimer = useRef | null>(null);\n const questionRefs = useRef<(HTMLDivElement | null)[]>([]);\n const measured = useRef(false);\n const [viewportH, setViewportH] = useState(undefined);\n const [trackY, setTrackY] = useState(0);\n const [animate, setAnimate] = useState(false);\n // Until the first question is measured, render only the active one so the\n // initial (and SSR) height is Q1's height — not all questions stacked, which\n // would flash to full height and then shrink on mount.\n const [ready, setReady] = useState(false);\n\n const last = qi === questions.length - 1;\n const selected = answers[qi] ?? [];\n const hasAnswer = selected.length > 0 || Boolean(custom[qi]?.trim());\n\n const sync = (withAnim: boolean) => {\n const item = questionRefs.current[qi];\n if (!item) return;\n const reduce = typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n setViewportH(item.offsetHeight);\n setTrackY(item.offsetTop);\n setAnimate(withAnim && !reduce);\n };\n\n useLayoutEffect(() => {\n const withAnim = measured.current;\n measured.current = true;\n sync(withAnim);\n setReady(true);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [qi, answers, custom, open, sent]);\n\n useEffect(() => {\n const id = requestAnimationFrame(() => sync(measured.current));\n return () => cancelAnimationFrame(id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [qi]);\n\n useEffect(() => () => { if (advanceTimer.current) clearTimeout(advanceTimer.current); }, []);\n\n const goTo = (next: number) => {\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setQi(Math.min(Math.max(next, 0), questions.length - 1));\n };\n\n const send = () => {\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n setSent(true);\n onSubmitted?.(answers);\n };\n\n const advance = () => {\n if (last) send();\n else goTo(qi + 1);\n };\n\n const toggle = (index: number) => {\n const type = questions[qi].type;\n setAnswers((current) => {\n const picked = current[qi] ?? [];\n const next = type === \"radio\"\n ? [index]\n : picked.includes(index)\n ? picked.filter((item) => item !== index)\n : [...picked, index];\n onAnswerChange?.(qi, next);\n return { ...current, [qi]: next };\n });\n if (type === \"radio\") {\n setCustom((current) => ({ ...current, [qi]: \"\" }));\n if (advanceTimer.current) clearTimeout(advanceTimer.current);\n advanceTimer.current = setTimeout(() => {\n if (last) send();\n else setQi((current) => Math.min(questions.length - 1, current + 1));\n }, 480);\n }\n };\n\n const reset = () => {\n setQi(0);\n setAnswers({});\n setCustom({});\n setSent(false);\n setOpen(true);\n measured.current = false;\n };\n\n if (!open) {\n return (\n \n );\n }\n\n if (sent) {\n return (\n
\n \n \n \n \n {t.sentMessage}\n \n {resettable && (\n \n )}\n
\n );\n }\n\n return (\n
\n
\n setOpen(false)}\n className=\"primitive-icon-button absolute right-2.5 top-2.5 z-10 text-ink-3 transition-colors duration-100 hover:bg-hover hover:text-ink\"\n >\n } />\n \n
\n {/* the question itself is the heading */}\n \n \n {questions.map((question, qIdx) => {\n const active = qIdx === qi;\n // Before the first measure, mount only the active question so the\n // card opens at its real height instead of flashing to full height.\n if (!ready && !active) return null;\n const picked = answers[qIdx] ?? [];\n const questionStyle: CSSProperties = {\n opacity: active ? 1 : 0,\n transition: animate ? `opacity ${SLIDE}` : undefined,\n pointerEvents: active ? undefined : \"none\",\n };\n return (\n { questionRefs.current[qIdx] = el; }}\n aria-hidden={active ? undefined : true}\n style={questionStyle}\n >\n
{question.q}
\n \n {question.options.map((option, i) => {\n const on = picked.includes(i);\n return (\n { if (active) toggle(i); }}\n className=\"relative z-10 flex items-center gap-1.5 rounded-control pl-1 pr-2 py-1 text-left transition-colors duration-100\"\n >\n \n {question.type === \"radio\" ? (\n \n ) : (\n \n )}\n \n \n {option}\n \n \n );\n })}\n \n \n
\n );\n })}\n
\n
\n
\n\n {/* footer — step nav (rolling counter) + pill actions */}\n
\n
\n goTo(qi - 1)}\n className=\"flex size-[18px] items-center justify-center rounded-[5px] transition-colors duration-100 enabled:hover:text-ink disabled:opacity-30\"\n >\n } />\n \n \n \n \n goTo(qi + 1)}\n className=\"flex size-[18px] items-center justify-center rounded-[5px] transition-colors duration-100 enabled:hover:text-ink disabled:opacity-30\"\n >\n } />\n \n
\n\n
\n \n \n
\n
\n \n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/ApprovalCard.tsx" - } - ] -} diff --git a/scripts/beautifului/button.json b/scripts/beautifului/button.json deleted file mode 100644 index a95acfa..0000000 --- a/scripts/beautifului/button.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "button", - "type": "registry:ui", - "title": "Button", - "description": "Shared building block used by other Beautiful UI components.", - "dependencies": [ - "class-variance-authority" - ], - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/atoms/Button.tsx", - "content": "\"use client\";\n\nimport { ButtonHTMLAttributes } from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst filledShadow = \"shadow-[inset_0_1px_0_rgba(255,255,255,0.14)]\";\n\n/* Pill-shaped by default — the app's core button style. Explicit symmetric\n * padding (not a fixed height) so the top/bottom spacing is always equal. */\nexport const buttonVariants = cva(\n `inline-flex items-center justify-center font-medium select-none\n transition-[transform,background-color,opacity] duration-150 ease-out\n active:scale-[0.96] disabled:opacity-50 disabled:pointer-events-none`,\n {\n variants: {\n variant: {\n primary: `bg-ink text-canvas hover:opacity-90 dark:bg-ink dark:text-canvas ${filledShadow}`,\n secondary: \"bg-surface text-ink shadow-btn hover:bg-inset aria-expanded:bg-hover\",\n ghost: \"bg-hover-2 text-ink hover:bg-line-strong\",\n accent: `bg-accent text-white hover:bg-accent-ink ${filledShadow}`,\n success: `bg-green text-white hover:brightness-95 ${filledShadow}`,\n /* transparent until hovered — for dense toolbars/action rows */\n quiet: \"text-ink hover:bg-hover\",\n },\n size: {\n /* compact toolbar pill — fixed height, lighter weight */\n xs: \"h-7 rounded-full px-2.5 text-[12px] font-normal leading-none gap-1\",\n /* canonical action pill — 27px tall, roomy sides */\n sm: \"h-[27px] px-3 text-[13px] leading-none rounded-full gap-1.5\",\n md: \"px-4 py-[9px] text-sm leading-none rounded-full gap-2\",\n },\n },\n defaultVariants: { variant: \"secondary\", size: \"md\" },\n },\n);\n\nexport type ButtonVariant = NonNullable[\"variant\"]>;\n\nexport function Button({\n variant,\n size,\n className,\n ...props\n}: ButtonHTMLAttributes & VariantProps) {\n return \n );\n })}\n {rows.length === 0 && (\n
\n No matches for “{query}”\n
\n )}\n
\n {menu === \"at\" ? \"Type to search sources & files\" : \"Type to search commands\"}\n
\n \n )}\n\n {/* ── model menu ─────────────────────────────────── */}\n {modelOpen && (\n setModelHovered(null)}\n className=\"absolute z-10 w-44 rounded-[10px] bg-surface p-1 shadow-raised\"\n style={{ left: modelMenuLeft, bottom: modelMenuBottom, animation: \"pop-in 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom left\" }}\n >\n {/* single gliding highlight — floats to the hovered / selected row */}\n \n {MODELS.map((m, i) => (\n {\n modelRowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => setModelHovered(i)}\n onClick={() => {\n selectModel(m);\n inputRef.current?.focus();\n }}\n className=\"relative z-10 flex h-7.5 w-full items-center gap-2 rounded-[6px] px-2 text-left\"\n >\n {m.name}\n {m.tag}\n \n \n \n \n ))}\n \n )}\n\n {/* ── composer ───────────────────────────────────── */}\n 0 || wide ? \"rounded-[24px]\" : \"rounded-full\") : tall ? \"rounded-[22px]\" : \"rounded-[14px]\"\n }`}\n >\n {/* rainbow glimm sweep — plays across the interior on model change.\n explicit w/h: a is a replaced element and won't stretch\n to inset-0 alone, which feeds back into the shader's ResizeObserver. */}\n \n \n {draft}\n \n\n {attachments.length > 0 && (\n
\n {attachments.map((file, i) => (\n \n \n {file}\n setAttachments((current) => current.filter((_, j) => j !== i))}\n className={`-my-1 flex size-6 items-center justify-center text-ink-3 transition-colors duration-100 hover:bg-line/70 hover:text-ink ${\n pill ? \"rounded-full\" : \"rounded-[5px]\"\n }`}\n >\n \n \n \n ))}\n
\n )}\n\n \n {\n setModelOpen(false);\n setPlusOpen((current) => !current);\n inputRef.current?.focus();\n }}\n className={`flex size-7 shrink-0 items-center justify-center justify-self-start text-ink-3 transition-[background-color,color,transform] duration-150 hover:bg-hover hover:text-ink active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${plusOpen ? \"bg-hover text-ink\" : \"\"} ${wide ? \"col-start-1 row-start-2\" : \"col-start-1 row-start-1\"}`}\n >\n \n \n\n {\n setDraft(event.target.value);\n setDismissed(false);\n setPlusOpen(false);\n }}\n onKeyDown={(event) => {\n if (menu && rows.length > 0) {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n setEngaged(true);\n setActive((current) => (current + (event.key === \"ArrowDown\" ? 1 : rows.length - 1)) % rows.length);\n return;\n }\n if ((event.key === \"Enter\" && !event.shiftKey) || event.key === \"Tab\") {\n event.preventDefault();\n pick(rows[active]);\n return;\n }\n }\n if (event.key === \"Escape\") {\n setDismissed(true);\n closeMenus();\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault();\n send();\n }\n }}\n placeholder={listening ? \"Listening…\" : placeholder ?? \"Write a message…\"}\n aria-label=\"Prompt\"\n className={`${tall ? \"min-h-[68px] px-2 py-2 text-[14px] leading-5\" : \"min-h-7 px-1 py-[5px] text-[13px] leading-[18px]\"} min-w-0 w-full resize-none bg-transparent text-ink outline-none [overflow-wrap:anywhere] placeholder:text-ink-3 ${\n wide ? \"col-span-full col-start-1 row-start-1\" : \"col-start-2 row-start-1\"\n }`}\n />\n\n {/* model picker */}\n {\n setPlusOpen(false);\n setModelOpen((current) => !current);\n }}\n className={`flex h-7 shrink-0 items-center gap-1 px-1.5 text-[12px] font-medium text-ink-2 transition-colors duration-150 hover:bg-hover hover:text-ink ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-2 row-start-2 justify-self-start\" : \"col-start-3 row-start-1\"}`}\n >\n {model.name}\n \n \n \n \n\n {/* dictation */}\n setListening((current) => !current)}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-150 active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${listening ? \"bg-accent-tint text-accent-ink\" : \"text-ink-3 hover:bg-hover hover:text-ink\"} ${wide ? \"col-start-4 row-start-2\" : \"col-start-4 row-start-1\"}`}\n >\n {listening ? (\n \n {[0, 1, 2].map((i) => (\n \n ))}\n \n ) : (\n \n )}\n \n\n {/* send — tactile square (round in the pill variant) */}\n \n \n \n \n \n \n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/PromptBar.tsx" - } - ] -} diff --git a/scripts/beautifului/registry.json b/scripts/beautifului/registry.json deleted file mode 100644 index 24c74b7..0000000 --- a/scripts/beautifului/registry.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "beautifui", - "homepage": "https://www.beautifului.dev", - "items": [ - { - "name": "foundation", - "type": "registry:style", - "title": "Beautiful UI foundation" - }, - { - "name": "button", - "type": "registry:ui", - "title": "Button" - }, - { - "name": "glide-menu", - "type": "registry:ui", - "title": "GlideMenu" - }, - { - "name": "entity-chip", - "type": "registry:ui", - "title": "EntityChip" - }, - { - "name": "value-pill", - "type": "registry:ui", - "title": "ValuePill" - }, - { - "name": "shimmer", - "type": "registry:ui", - "title": "Shimmer" - }, - { - "name": "stream-text", - "type": "registry:ui", - "title": "StreamText" - }, - { - "name": "loading-state", - "type": "registry:component", - "title": "Loading State" - }, - { - "name": "thinking-state", - "type": "registry:component", - "title": "Thinking" - }, - { - "name": "streaming-text", - "type": "registry:component", - "title": "Streaming Text" - }, - { - "name": "approval-card", - "type": "registry:component", - "title": "Approval Card" - }, - { - "name": "tool-chips", - "type": "registry:component", - "title": "Tool Chips" - }, - { - "name": "task-rows", - "type": "registry:component", - "title": "Task Rows" - }, - { - "name": "chat-composer", - "type": "registry:component", - "title": "Chat" - }, - { - "name": "prompt-bar", - "type": "registry:component", - "title": "Prompt Bar" - }, - { - "name": "recommendation-card", - "type": "registry:component", - "title": "Recommendation Card" - }, - { - "name": "context-cards", - "type": "registry:component", - "title": "Context Cards" - }, - { - "name": "diff-table", - "type": "registry:component", - "title": "Diff Table" - }, - { - "name": "records-table", - "type": "registry:component", - "title": "Records Table" - }, - { - "name": "filter-table", - "type": "registry:component", - "title": "Filter Table" - }, - { - "name": "sidebar-nav", - "type": "registry:component", - "title": "Sidebar Nav" - }, - { - "name": "search", - "type": "registry:component", - "title": "Search" - }, - { - "name": "flowchart", - "type": "registry:component", - "title": "Flowchart" - }, - { - "name": "insight-cards", - "type": "registry:component", - "title": "Insight Cards" - }, - { - "name": "code-block", - "type": "registry:component", - "title": "Code Block" - }, - { - "name": "fine-tune-card", - "type": "registry:component", - "title": "Fine-tune Card" - }, - { - "name": "selection-actions", - "type": "registry:component", - "title": "Selection Actions" - } - ] -} diff --git a/scripts/beautifului/selection-actions.json b/scripts/beautifului/selection-actions.json deleted file mode 100644 index 43ca545..0000000 --- a/scripts/beautifului/selection-actions.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "selection-actions", - "type": "registry:component", - "title": "Selection Actions", - "description": "Highlight a passage and hand it to the agent to rewrite.", - "dependencies": [ - "iconoir-react" - ], - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json", - "https://www.beautifului.dev/r/button.json", - "https://www.beautifului.dev/r/shimmer.json", - "https://www.beautifului.dev/r/stream-text.json" - ], - "files": [ - { - "path": "components/primitives/SelectionActions.tsx", - "content": "\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n ArrowUp,\n ChatBubbleQuestion,\n Check,\n EmojiSatisfied,\n NavArrowRight,\n Refresh,\n Scissor,\n Spark,\n TextBox,\n Xmark,\n} from \"iconoir-react\";\nimport { Button } from \"@/components/atoms/Button\";\nimport { Shimmer } from \"@/components/atoms/Shimmer\";\nimport { StreamText } from \"@/components/atoms/StreamText\";\n\n/* ─────────────────────────────────────────────────────────\n * SELECTION ACTIONS\n * A contextual AI bar attached beneath selected text.\n * The global theme owns its surface; this component only\n * composes existing surface, ink, accent, radius and motion\n * tokens.\n * ───────────────────────────────────────────────────────── */\n\nconst LEAD = \"Pistachio holds the top slot all weekend. \";\nconst PICKED =\n \"Churn it first thing Saturday so the batch has time to firm up before the afternoon rush.\";\nconst REWRITE =\n \"Churn pistachio first thing Saturday so the batch has time to fully firm before the afternoon rush.\";\n\n/* The passage: lead-in text, the selected `original`, and the streamed `rewrite`. */\nexport type SelectionText = {\n lead: string;\n original: string;\n rewrite: string;\n};\n\n/* A single AI action offered in the bar. Omit `action` for a no-op button\n * (e.g. Explain); `busyLabel` is the gerund shown while it runs. */\nexport type SelectionAction = {\n id: string;\n icon: ReactNode;\n action?: string;\n busyLabel?: string;\n};\n\n/* The action set: `primary` are always visible; `more` reveal on expand. */\nexport type SelectionActionSet = {\n primary: SelectionAction[];\n more: SelectionAction[];\n};\n\n/* Prominent copy strings. */\nexport type SelectionActionsLabels = {\n keep: string;\n discard: string;\n placeholder: string;\n};\n\nconst DEFAULT_TEXT: SelectionText = {\n lead: LEAD,\n original: PICKED,\n rewrite: REWRITE,\n};\n\nconst DEFAULT_LABELS: SelectionActionsLabels = {\n keep: \"Keep\",\n discard: \"Discard\",\n placeholder: \"Describe edits\",\n};\n\ntype Mode = \"idle\" | \"thinking\" | \"streaming\" | \"result\";\n\nconst iconProps = {\n width: 14,\n height: 14,\n strokeWidth: 1.8,\n \"aria-hidden\": true,\n} as const;\n\nconst icons = {\n explain: ,\n improve: ,\n shorten: ,\n tone: ,\n grammar: ,\n send: (\n \n ),\n chevron: ,\n check: ,\n close: ,\n retry: ,\n};\n\n/* the single \"keep\" affirm — solid ink with a hairline (not the atom's filled\n * highlight) shadow, so it stays a local one-off rather than a Button variant */\nconst primary =\n \"inline-flex h-7 shrink-0 items-center gap-1 rounded-full bg-ink px-2.5 text-[12.5px] font-normal text-canvas shadow-hairline transition-[opacity,transform] duration-150 hover:opacity-90 active:scale-[0.96]\";\n\n\nconst DEFAULT_ACTIONS: SelectionActionSet = {\n primary: [\n { id: \"Explain\", icon: icons.explain },\n { id: \"Improve\", icon: icons.improve, action: \"Improve\", busyLabel: \"Improving\" },\n ],\n more: [\n { id: \"Shorten\", icon: icons.shorten, action: \"Shorten\", busyLabel: \"Shortening\" },\n { id: \"Tone\", icon: icons.tone, action: \"Change tone\", busyLabel: \"Changing tone\" },\n { id: \"Grammar\", icon: icons.grammar, action: \"Fix grammar\" },\n ],\n};\n\nexport type SelectionActionsProps = {\n /** Accepted for gallery/registry parity; not used by this bar. */\n variant?: string;\n /** The passage shown above the bar. */\n text?: Partial;\n /** The AI actions offered in the bar. */\n actions?: SelectionActionSet;\n /** Prominent copy strings. */\n labels?: Partial;\n /** Called with the action name whenever an edit is run. */\n onAction?: (action: string) => void;\n};\n\nexport default function SelectionActions({\n text: textProp,\n actions = DEFAULT_ACTIONS,\n labels,\n onAction,\n}: SelectionActionsProps = {}) {\n const passage = { ...DEFAULT_TEXT, ...textProp };\n const copy = { ...DEFAULT_LABELS, ...labels };\n const [shown, setShown] = useState(false);\n const [mode, setMode] = useState(\"idle\");\n const [action, setAction] = useState(\"Improve\");\n const [prompt, setPrompt] = useState(\"\");\n const [typingWidth, setTypingWidth] = useState(null);\n const [expanded, setExpanded] = useState(false);\n const [anchor, setAnchor] = useState({ x: 0, y: 0 });\n const [positioned, setPositioned] = useState(false);\n\n const hostRef = useRef(null);\n const selectionRef = useRef(null);\n const barRef = useRef(null);\n const contentRef = useRef(null);\n const frameRef = useRef(null);\n const previousModeRef = useRef(\"idle\");\n const lastWidthRef = useRef(0);\n const widthAnimationRef = useRef(null);\n\n useEffect(() => {\n const timer = window.setTimeout(() => setShown(true), 280);\n return () => window.clearTimeout(timer);\n }, []);\n\n useEffect(() => {\n if (mode !== \"thinking\") return;\n const timer = window.setTimeout(() => setMode(\"streaming\"), 700);\n return () => window.clearTimeout(timer);\n }, [mode]);\n\n /* Attach beneath the final selected line, while centering the bar\n * against the complete selection bounds. requestAnimationFrame batches\n * streaming reflow measurements and avoids visible intermediate positions. */\n const place = useCallback(() => {\n if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);\n frameRef.current = requestAnimationFrame(() => {\n const host = hostRef.current;\n const selection = selectionRef.current;\n if (!host || !selection) return;\n\n const bounds = selection.getBoundingClientRect();\n const lines = Array.from(selection.getClientRects());\n const lastLine = lines.at(-1);\n if (!lastLine) return;\n\n const hostBounds = host.getBoundingClientRect();\n const next = {\n x: Math.round(bounds.left - hostBounds.left + bounds.width / 2),\n y: Math.round(lastLine.bottom - hostBounds.top + 8),\n };\n\n setAnchor((current) =>\n current.x === next.x && current.y === next.y ? current : next,\n );\n setPositioned(true);\n });\n }, []);\n\n useLayoutEffect(() => {\n place();\n }, [mode, place]);\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(place);\n observer.observe(host);\n window.addEventListener(\"resize\", place);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"resize\", place);\n if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);\n };\n }, [place]);\n\n /* Intrinsic width handles the preset expansion. When the entire content\n * changes between idle, loading and confirmation, animate from the last\n * rendered width to the new intrinsic width before the browser paints. */\n useLayoutEffect(() => {\n const bar = barRef.current;\n const content = contentRef.current;\n if (!bar || !content) return;\n\n const nextWidth = Math.ceil(content.getBoundingClientRect().width) + 8;\n const previousWidth =\n lastWidthRef.current || Math.ceil(bar.getBoundingClientRect().width);\n\n if (\n previousModeRef.current !== mode &&\n Math.abs(nextWidth - previousWidth) > 1\n ) {\n widthAnimationRef.current?.cancel();\n const animation = bar.animate(\n [\n { width: `${previousWidth}px` },\n { width: `${nextWidth}px` },\n ],\n {\n duration: 320,\n easing: \"cubic-bezier(0.23,1,0.32,1)\",\n },\n );\n widthAnimationRef.current = animation;\n animation.onfinish = () => {\n lastWidthRef.current = nextWidth;\n widthAnimationRef.current = null;\n };\n } else {\n lastWidthRef.current = nextWidth;\n }\n\n previousModeRef.current = mode;\n }, [mode]);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n\n const observer = new ResizeObserver(() => {\n if (widthAnimationRef.current?.playState === \"running\") return;\n lastWidthRef.current =\n Math.ceil(content.getBoundingClientRect().width) + 8;\n });\n observer.observe(content);\n return () => {\n observer.disconnect();\n widthAnimationRef.current?.cancel();\n };\n }, []);\n\n const run = (nextAction: string) => {\n setAction(nextAction);\n setExpanded(false);\n setMode(\"thinking\");\n onAction?.(nextAction);\n };\n\n const reset = () => {\n setExpanded(false);\n setPrompt(\"\");\n setTypingWidth(null);\n setAction(\"Improve\");\n setMode(\"idle\");\n };\n\n const busy = mode === \"thinking\" || mode === \"streaming\";\n const visible = shown && positioned;\n const hasPrompt = prompt.trim().length > 0;\n const busyLabelMap: Record = {};\n for (const item of [...actions.primary, ...actions.more]) {\n if (item.action && item.busyLabel) busyLabelMap[item.action] = item.busyLabel;\n }\n const busyLabel = busyLabelMap[action] ?? \"Editing\";\n\n return (\n
\n
\n

\n {passage.lead}\n \n {mode === \"idle\" || mode === \"thinking\" ? (\n passage.original\n ) : mode === \"streaming\" ? (\n setMode(\"result\")}\n />\n ) : (\n passage.rewrite\n )}\n \n

\n\n \n {/* A 36px pill wraps 28px controls at a 4px inset. The controls\n resolve to a 14px radius, preserving the concentric curve. */}\n \n \n {busy && (\n \n \n {mode === \"thinking\" ? (\n \n {busyLabel}…\n \n ) : (\n {busyLabel}…\n )}\n \n )}\n\n {mode === \"result\" && (\n <>\n \n {icons.check}\n {copy.keep}\n \n \n \n run(action)}\n className=\"flex size-7 shrink-0 items-center justify-center rounded-full text-ink-3 transition-[background-color,color,transform] duration-150 hover:bg-hover-2 hover:text-ink-2 active:scale-[0.96]\"\n >\n {icons.retry}\n \n \n )}\n\n {mode === \"idle\" && (\n <>\n \n {\n event.preventDefault();\n run(prompt.trim() || \"Improve\");\n }}\n >\n {\n const next = event.target.value;\n if (!prompt.trim() && next.trim()) {\n setTypingWidth(\n Math.ceil(\n barRef.current?.getBoundingClientRect().width ??\n 0,\n ),\n );\n } else if (!next.trim()) {\n setTypingWidth(null);\n }\n setPrompt(next);\n }}\n aria-label={copy.placeholder}\n placeholder={copy.placeholder}\n className=\"h-7 w-full bg-transparent pr-2.5 pl-3 text-[12.5px] text-ink placeholder:text-ink-3\"\n />\n \n
\n\n \n {!expanded && (\n \n )}\n {actions.primary.map((item) => (\n run(item.action!) : undefined}\n >\n {item.icon}\n {item.id}\n \n ))}\n\n \n {actions.more.map((item) => (\n run(item.action!) : undefined}\n >\n {item.icon}\n {item.id}\n \n ))}\n
\n\n \n setExpanded((value) => !value)}\n className=\"flex size-7 shrink-0 items-center justify-center rounded-full text-ink transition-[background-color,transform] duration-200 hover:bg-hover active:scale-[0.96]\"\n >\n \n {icons.chevron}\n \n \n \n\n \n run(prompt.trim())}\n className=\"flex size-7 shrink-0 items-center justify-center rounded-full bg-ink text-surface transition-[opacity,transform] duration-200 active:scale-[0.94]\"\n >\n {icons.send}\n \n \n \n )}\n \n \n \n \n\n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/SelectionActions.tsx" - } - ] -} diff --git a/scripts/beautifului/shimmer.json b/scripts/beautifului/shimmer.json deleted file mode 100644 index fbefee4..0000000 --- a/scripts/beautifului/shimmer.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "shimmer", - "type": "registry:ui", - "title": "Shimmer", - "description": "Shared building block used by other Beautiful UI components.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/atoms/Shimmer.tsx", - "content": "/** Shimmering label — signals the agent is processing. */\nexport function Shimmer({\n children,\n className = \"\",\n}: {\n children: React.ReactNode;\n className?: string;\n}) {\n return (\n \n {children}\n \n );\n}\n", - "type": "registry:ui", - "target": "components/atoms/Shimmer.tsx" - } - ] -} diff --git a/scripts/beautifului/sidebar-nav.json b/scripts/beautifului/sidebar-nav.json deleted file mode 100644 index 59ed11f..0000000 --- a/scripts/beautifului/sidebar-nav.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "sidebar-nav", - "type": "registry:component", - "title": "Sidebar Nav", - "description": "Collapsible workspace and chat navigation with gliding hover states. Then add `@import \"./beautifui/sidebar-nav.css\";` to your app's globals.css.", - "dependencies": [ - "@central-icons-react/round-outlined-radius-2-stroke-2" - ], - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json", - "https://www.beautifului.dev/r/glide-menu.json" - ], - "files": [ - { - "path": "components/primitives/SidebarNav.tsx", - "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { IconArrowBoxLeft } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconArrowBoxLeft\";\nimport { IconCheckmark1Small } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconCheckmark1Small\";\nimport { IconChevronDownSmall } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconChevronDownSmall\";\nimport { IconCrossSmall } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconCrossSmall\";\nimport { IconEditBig } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconEditBig\";\nimport { IconHome } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconHome\";\nimport { IconMagnifyingGlass } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconMagnifyingGlass\";\nimport { IconPlusMedium } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconPlusMedium\";\nimport { IconPopsicle2 } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconPopsicle2\";\nimport { IconSettingsGear1 } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconSettingsGear1\";\nimport { IconSidebarLeftArrow } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconSidebarLeftArrow\";\nimport { IconUserAdd } from \"@central-icons-react/round-outlined-radius-2-stroke-2/IconUserAdd\";\nimport GlideMenu from \"@/components/primitives/GlideMenu\";\n\n/* ─────────────────────────────────────────────────────────\n * SIDEBAR NAV\n * Shared by the design-system preview and the harness shell:\n * compact workspace switcher, primary navigation, searchable\n * chat history, and a collapse that preserves icon alignment.\n * ───────────────────────────────────────────────────────── */\n\nconst WORKSPACE = { key: \"creamery\", name: \"Creamery Ops\", monogram: \"C\" };\n\nconst NAV_ITEMS = [\n { key: \"home\", label: \"Home\", icon: },\n { key: \"invite\", label: \"Invite users\", icon: , count: \"3/10\" },\n];\n\nexport type SidebarRecent = {\n id: string;\n label: string;\n prompt?: string;\n};\n\nconst DEFAULT_RECENTS: SidebarRecent[] = [\n { id: \"suppliers\", label: \"Supplier records\" },\n { id: \"todos\", label: \"Urgent to-dos this morning\" },\n { id: \"flavor\", label: \"Flavor page ticket\" },\n { id: \"workload\", label: \"Workload summary\" },\n { id: \"offboarding\", label: \"Off-board a supplier\" },\n { id: \"restock\", label: \"Batch restock function\" },\n { id: \"edits\", label: \"Propose flavor edits\" },\n { id: \"subway\", label: \"Subway surfing\" },\n];\n\ntype SidebarNavProps = {\n activeTitle?: string | null;\n className?: string;\n fill?: boolean;\n onNewChat?: () => void;\n onPick?: (id: string, label: string, prompt?: string) => void;\n /** controlled primary-nav selection (e.g. \"home\" | \"invite\") */\n activeNav?: string;\n onNavigate?: (key: string) => void;\n /** footer call-to-action — defaults to the demo \"Upgrade\" button */\n footerLabel?: string;\n footerIcon?: ReactNode;\n onFooterClick?: () => void;\n recents?: SidebarRecent[];\n variant?: string;\n};\n\nconst SIDEBAR_MOTION = {\n expandedWidth: 224,\n collapsedWidth: 52,\n duration: 280,\n copyDuration: 180,\n copyOffset: 8,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\n/* ─────────────────────────────────────────────────────────\n * CHAT SEARCH STORYBOARD\n *\n * 0ms search is triggered; Chats label begins fading\n * 0ms field grows right → left from the search control\n * 180ms field fills the row; cursor is focused and ready\n * ───────────────────────────────────────────────────────── */\nconst CHAT_SEARCH_MOTION = {\n duration: 180,\n closedWidth: 28,\n easing: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n};\n\nfunction GlideGroup({ children }: { children: ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nfunction RailButton({\n icon,\n label,\n active = false,\n count,\n onClick,\n}: {\n icon: ReactNode;\n label: string;\n active?: boolean;\n count?: string;\n onClick?: () => void;\n}) {\n return (\n \n \n {icon}\n \n \n {label}\n \n {count && (\n \n {count}\n \n )}\n \n );\n}\n\nfunction WorkspaceMenu({\n position,\n onClose,\n}: {\n position: { top: number; left: number };\n onClose: () => void;\n}) {\n return createPortal(\n \n \n \n \n {WORKSPACE.monogram}\n \n {WORKSPACE.name}\n \n \n
\n {[\n { label: \"New workspace\", icon: },\n { label: \"Workspace settings\", icon: },\n { label: \"Invite team members\", icon: },\n ].map((item) => (\n \n {item.icon}\n {item.label}\n \n ))}\n
\n \n \n Sign out\n \n \n
,\n document.body,\n );\n}\n\nexport default function SidebarNav({\n activeTitle,\n className = \"\",\n fill = false,\n onNewChat,\n onPick,\n activeNav,\n onNavigate,\n footerLabel = \"Upgrade\",\n footerIcon,\n onFooterClick,\n recents = DEFAULT_RECENTS,\n}: SidebarNavProps) {\n const [collapsed, setCollapsed] = useState(false);\n const [internalNav, setInternalNav] = useState(\"chats\");\n const currentNav = activeNav ?? internalNav;\n const selectNav = (key: string) => {\n setInternalNav(key);\n onNavigate?.(key);\n };\n const [demoActiveTitle, setDemoActiveTitle] = useState(null);\n const [workspaceOpen, setWorkspaceOpen] = useState(false);\n const [workspacePosition, setWorkspacePosition] = useState({ top: 0, left: 0 });\n const [searchOpen, setSearchOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const workspaceButtonRef = useRef(null);\n const searchRef = useRef(null);\n\n const selectedTitle = activeTitle === undefined ? demoActiveTitle : activeTitle;\n const visibleRecents = recents.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()));\n\n useEffect(() => {\n if (!workspaceOpen) return;\n const close = (event: PointerEvent) => {\n const target = event.target as Element;\n if (!target.closest(\"[data-workspace-trigger]\") && !target.closest(\"[data-workspace-menu]\")) {\n setWorkspaceOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [workspaceOpen]);\n\n useEffect(() => {\n if (searchOpen) searchRef.current?.focus();\n }, [searchOpen]);\n\n const collapse = () => {\n setCollapsed(true);\n setWorkspaceOpen(false);\n setSearchOpen(false);\n setQuery(\"\");\n };\n\n return (\n \n
\n
\n {\n if (!workspaceOpen && workspaceButtonRef.current) {\n const rect = workspaceButtonRef.current.getBoundingClientRect();\n setWorkspacePosition({ top: rect.bottom + 6, left: rect.left });\n }\n setWorkspaceOpen((open) => !open);\n }}\n className=\"sidebar-workspace-control absolute left-2 top-1 flex h-8 w-[164px] items-center rounded-[8px] px-2 text-left transition-[background-color,transform] duration-100 hover:bg-hover-2 active:scale-[0.99]\"\n >\n \n \n \n \n {WORKSPACE.name}\n \n \n \n \n \n\n {workspaceOpen && setWorkspaceOpen(false)} />}\n\n \n \n \n setCollapsed(false)}\n className=\"sidebar-expand-control absolute left-2 top-0.5 flex size-9 items-center justify-center rounded-[8px] text-ink-3 transition-[opacity,background-color,color] duration-150 hover:bg-hover-2 hover:text-ink\"\n >\n \n \n
\n\n \n }\n label=\"New chat\"\n onClick={() => {\n if (activeTitle === undefined) setDemoActiveTitle(null);\n selectNav(\"chats\");\n onNewChat?.();\n }}\n />\n {NAV_ITEMS.map((item) => (\n selectNav(item.key)}\n />\n ))}\n \n\n
\n
\n \n \n Chats\n
\n\n setSearchOpen(true)}\n className={`absolute right-0 top-0 z-10 flex size-8 items-center justify-center rounded-[8px] text-ink-3 transition-[opacity,background-color,color,transform] hover:bg-hover-2 hover:text-ink active:scale-[0.96] ${searchOpen ? \"pointer-events-none opacity-0\" : \"opacity-100\"}`}\n style={{ transitionDuration: `${CHAT_SEARCH_MOTION.duration}ms` }}\n >\n \n \n\n \n \n \n \n setQuery(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setSearchOpen(false);\n setQuery(\"\");\n }\n }}\n placeholder=\"Search chats\"\n aria-label=\"Search chat history\"\n className=\"ml-1.5 min-w-0 flex-1 bg-transparent text-[13px] font-medium text-ink outline-none placeholder:text-ink-3\"\n />\n {\n setSearchOpen(false);\n setQuery(\"\");\n }}\n className=\"flex size-8 shrink-0 items-center justify-center rounded-[8px] text-ink-3 transition-[background-color,color,transform] duration-150 hover:bg-hover-2 hover:text-ink active:scale-[0.96]\"\n >\n \n \n
\n
\n\n \n {visibleRecents.map((item) => {\n const active = item.label === selectedTitle;\n return (\n {\n selectNav(\"chats\");\n if (activeTitle === undefined) setDemoActiveTitle(item.label);\n onPick?.(item.id, item.label, item.prompt);\n }}\n className={`sidebar-row relative z-10 mx-2 flex h-8 items-center rounded-[8px] px-2 text-left transition-[width,background-color,color,transform] duration-150 active:scale-[0.98] ${\n active ? \"bg-hover-2 group-hover/glide:bg-transparent\" : \"\"\n }`}\n >\n \n {item.label}\n \n \n );\n })}\n {query && visibleRecents.length === 0 && (\n
No chats found
\n )}\n
\n
\n\n
\n \n {footerIcon}\n {footerLabel}\n \n
\n \n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/SidebarNav.tsx" - }, - { - "path": "app/beautifui/sidebar-nav.css", - "content": "/* ══════════════════════════════════════════════════════════\n * COMPONENT-SPECIFIC — SidebarNav\n * The 52↔224px collapsible sidebar tree.\n * Copy this block only alongside SidebarNav; it is not part of the\n * generic foundation above.\n * ══════════════════════════════════════════════════════════ */\n.sidebar-row {\n width: 208px;\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: var(--sidebar-easing);\n}\n\n.sidebar-copy {\n opacity: 1;\n transform: translateX(0);\n transition:\n opacity var(--sidebar-copy-duration) ease-out,\n transform var(--sidebar-copy-duration) var(--sidebar-easing);\n}\n\n.sidebar-logo,\n.sidebar-collapse-control,\n.sidebar-expand-control {\n transition-duration: var(--sidebar-copy-duration);\n transition-timing-function: ease-out;\n}\n\n.sidebar-expand-control {\n pointer-events: none;\n opacity: 0;\n}\n\n.sidebar-glide-highlight {\n right: 8px;\n left: 8px;\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-row {\n width: 36px;\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-copy {\n pointer-events: none;\n opacity: 0;\n transform: translateX(calc(var(--sidebar-copy-offset) * -1));\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-workspace-control {\n pointer-events: none;\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-logo,\n[data-sidebar-collapsed=\"true\"] .sidebar-collapse-control {\n pointer-events: none;\n opacity: 0;\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-expand-control {\n pointer-events: auto;\n opacity: 1;\n}\n\n[data-sidebar-collapsed=\"true\"] .sidebar-glide-highlight {\n right: auto;\n width: 36px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .home-reveal {\n opacity: 1 !important;\n transform: none !important;\n filter: none !important;\n }\n}\n\n/* ── records table ────────────────────────────────────────\n * The grid is deliberately more architectural than card-like:\n * one quiet surface, precise rules, and color reserved for\n * meaning. The first column remains an anchor while both axes\n * can move independently inside the table viewport.\n */\n", - "type": "registry:file", - "target": "app/beautifui/sidebar-nav.css" - } - ] -} diff --git a/scripts/beautifului/stage/ApprovalCard.tsx b/scripts/beautifului/stage/ApprovalCard.tsx deleted file mode 100644 index 0996205..0000000 --- a/scripts/beautifului/stage/ApprovalCard.tsx +++ /dev/null @@ -1,419 +0,0 @@ -"use client"; - -import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react"; -import { Button } from "@/components/atoms/Button"; -import GlideMenu from "@/components/primitives/GlideMenu"; - -/* ───────────────────────────────────────────────────────── - * APPROVAL CARD (human-in-the-loop) - * One question at a time. The stack slides vertically as you - * move between questions (the card's height animates to fit), - * the step counter rolls like an odometer, and the footer uses - * pill actions — a quiet Skip and a dark Continue with a ⏎. - * Single-choice answers auto-advance; multi-select waits. - * ───────────────────────────────────────────────────────── */ - -export type ApprovalQuestion = { - q: string; - type: "radio" | "check"; - options: string[]; -}; - -const QUESTIONS: ApprovalQuestion[] = [ - { - q: "How many flavors should we launch?", - type: "radio", - options: ["Three (core line)", "Five (full case)", "Just one hero"], - }, - { - q: "Which mix-ins should we stock?", - type: "check", - options: ["Chocolate chips", "Waffle bits", "Sprinkles"], - }, - { - q: "Which market do we enter first?", - type: "radio", - options: ["Food trucks", "Grocery freezers", "Scoop shops"], - }, -]; - -export type ApprovalLabels = { - skip: string; - continue: string; - send: string; - customPlaceholder: string; - sentMessage: string; -}; - -const DEFAULT_LABELS: ApprovalLabels = { - skip: "Skip", - continue: "Continue", - send: "Send", - customPlaceholder: "Something else…", - sentMessage: "Answers sent", -}; - -const ROLL_MS = 400; -const SLIDE = "360ms cubic-bezier(0.22, 1, 0.36, 1)"; - -/* odometer digits — each character that changes rolls up (or down) */ -function RollingDigits({ value }: { value: string }) { - const prevRef = useRef(value); - const [oldVal, setOldVal] = useState(value); - const [newVal, setNewVal] = useState(value); - const [rolling, setRolling] = useState(false); - const [shifted, setShifted] = useState(false); - const [dir, setDir] = useState<"up" | "down">("up"); - - useEffect(() => { - if (prevRef.current === value) return; - const from = prevRef.current; - prevRef.current = value; - const fromN = parseInt(from, 10); - const toN = parseInt(value, 10); - setDir(Number.isFinite(fromN) && Number.isFinite(toN) && toN < fromN ? "down" : "up"); - setOldVal(from); - setNewVal(value); - setRolling(true); - setShifted(false); - - let raf2 = 0; - const raf1 = requestAnimationFrame(() => { - raf2 = requestAnimationFrame(() => setShifted(true)); - }); - const done = setTimeout(() => { - setRolling(false); - setOldVal(value); - setShifted(false); - }, ROLL_MS); - - return () => { - cancelAnimationFrame(raf1); - cancelAnimationFrame(raf2); - clearTimeout(done); - }; - }, [value]); - - const chars = rolling ? newVal : oldVal; - - return ( - <> - {Array.from({ length: chars.length }, (_, i) => { - const o = oldVal[i] ?? ""; - const n = chars[i] ?? ""; - if (!rolling || o === n) { - return {n}; - } - const top = dir === "down" ? n : o; - const bottom = dir === "down" ? o : n; - const restY = dir === "down" ? "0" : "-1em"; - const startY = dir === "down" ? "-1em" : "0"; - return ( - - - {top} - {bottom} - - - ); - })} - - ); -} - -function Ico({ path, size = 14, sw = 2 }: { path: React.ReactNode; size?: number; sw?: number }) { - return ( - - {path} - - ); -} - -export default function ApprovalCard({ - questions = QUESTIONS, - labels, - onSubmitted, - onAnswerChange, - resettable = true, -}: { - questions?: ApprovalQuestion[]; - labels?: Partial; - onSubmitted?: (answers: Record) => void; - onAnswerChange?: (questionIndex: number, answer: number[]) => void; - resettable?: boolean; - variant?: string; -} = {}) { - const t = { ...DEFAULT_LABELS, ...labels }; - const [qi, setQi] = useState(0); - const [answers, setAnswers] = useState>({}); - const [custom, setCustom] = useState>({}); - const [sent, setSent] = useState(false); - const [open, setOpen] = useState(true); - - const advanceTimer = useRef | null>(null); - const questionRefs = useRef<(HTMLDivElement | null)[]>([]); - const measured = useRef(false); - const [viewportH, setViewportH] = useState(undefined); - const [trackY, setTrackY] = useState(0); - const [animate, setAnimate] = useState(false); - // Until the first question is measured, render only the active one so the - // initial (and SSR) height is Q1's height — not all questions stacked, which - // would flash to full height and then shrink on mount. - const [ready, setReady] = useState(false); - - const last = qi === questions.length - 1; - const selected = answers[qi] ?? []; - const hasAnswer = selected.length > 0 || Boolean(custom[qi]?.trim()); - - const sync = (withAnim: boolean) => { - const item = questionRefs.current[qi]; - if (!item) return; - const reduce = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - setViewportH(item.offsetHeight); - setTrackY(item.offsetTop); - setAnimate(withAnim && !reduce); - }; - - useLayoutEffect(() => { - const withAnim = measured.current; - measured.current = true; - sync(withAnim); - setReady(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [qi, answers, custom, open, sent]); - - useEffect(() => { - const id = requestAnimationFrame(() => sync(measured.current)); - return () => cancelAnimationFrame(id); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [qi]); - - useEffect(() => () => { if (advanceTimer.current) clearTimeout(advanceTimer.current); }, []); - - const goTo = (next: number) => { - if (advanceTimer.current) clearTimeout(advanceTimer.current); - setQi(Math.min(Math.max(next, 0), questions.length - 1)); - }; - - const send = () => { - if (advanceTimer.current) clearTimeout(advanceTimer.current); - setSent(true); - onSubmitted?.(answers); - }; - - const advance = () => { - if (last) send(); - else goTo(qi + 1); - }; - - const toggle = (index: number) => { - const type = questions[qi].type; - setAnswers((current) => { - const picked = current[qi] ?? []; - const next = type === "radio" - ? [index] - : picked.includes(index) - ? picked.filter((item) => item !== index) - : [...picked, index]; - onAnswerChange?.(qi, next); - return { ...current, [qi]: next }; - }); - if (type === "radio") { - setCustom((current) => ({ ...current, [qi]: "" })); - if (advanceTimer.current) clearTimeout(advanceTimer.current); - advanceTimer.current = setTimeout(() => { - if (last) send(); - else setQi((current) => Math.min(questions.length - 1, current + 1)); - }, 480); - } - }; - - const reset = () => { - setQi(0); - setAnswers({}); - setCustom({}); - setSent(false); - setOpen(true); - measured.current = false; - }; - - if (!open) { - return ( - - ); - } - - if (sent) { - return ( -
- - - - - {t.sentMessage} - - {resettable && ( - - )} -
- ); - } - - return ( -
-
- -
- {/* the question itself is the heading */} -
-
- {questions.map((question, qIdx) => { - const active = qIdx === qi; - // Before the first measure, mount only the active question so the - // card opens at its real height instead of flashing to full height. - if (!ready && !active) return null; - const picked = answers[qIdx] ?? []; - const questionStyle: CSSProperties = { - opacity: active ? 1 : 0, - transition: animate ? `opacity ${SLIDE}` : undefined, - pointerEvents: active ? undefined : "none", - }; - return ( -
{ questionRefs.current[qIdx] = el; }} - aria-hidden={active ? undefined : true} - style={questionStyle} - > -
{question.q}
- - {question.options.map((option, i) => { - const on = picked.includes(i); - return ( - - ); - })} - - -
- ); - })} -
-
-
- - {/* footer — step nav (rolling counter) + pill actions */} -
-
- - - - - -
- -
- - -
-
-
-
- ); -} diff --git a/scripts/beautifului/stage/Button.tsx b/scripts/beautifului/stage/Button.tsx deleted file mode 100644 index af0bfca..0000000 --- a/scripts/beautifului/stage/Button.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { ButtonHTMLAttributes } from "react"; -import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils"; - -const filledShadow = "shadow-[inset_0_1px_0_rgba(255,255,255,0.14)]"; - -/* Pill-shaped by default — the app's core button style. Explicit symmetric - * padding (not a fixed height) so the top/bottom spacing is always equal. */ -export const buttonVariants = cva( - `inline-flex items-center justify-center font-medium select-none - transition-[transform,background-color,opacity] duration-150 ease-out - active:scale-[0.96] disabled:opacity-50 disabled:pointer-events-none`, - { - variants: { - variant: { - primary: `bg-ink text-canvas hover:opacity-90 dark:bg-ink dark:text-canvas ${filledShadow}`, - secondary: "bg-surface text-ink shadow-btn hover:bg-inset aria-expanded:bg-hover", - ghost: "bg-hover-2 text-ink hover:bg-line-strong", - accent: `bg-accent text-white hover:bg-accent-ink ${filledShadow}`, - success: `bg-green text-white hover:brightness-95 ${filledShadow}`, - /* transparent until hovered — for dense toolbars/action rows */ - quiet: "text-ink hover:bg-hover", - }, - size: { - /* compact toolbar pill — fixed height, lighter weight */ - xs: "h-7 rounded-full px-2.5 text-[12px] font-normal leading-none gap-1", - /* canonical action pill — 27px tall, roomy sides */ - sm: "h-[27px] px-3 text-[13px] leading-none rounded-full gap-1.5", - md: "px-4 py-[9px] text-sm leading-none rounded-full gap-2", - }, - }, - defaultVariants: { variant: "secondary", size: "md" }, - }, -); - -export type ButtonVariant = NonNullable["variant"]>; - -export function Button({ - variant, - size, - className, - ...props -}: ButtonHTMLAttributes & VariantProps) { - return - ); -} - -function WorkspaceMenu({ - position, - onClose, -}: { - position: { top: number; left: number }; - onClose: () => void; -}) { - return createPortal( -
- - -
- {[ - { label: "New workspace", icon: }, - { label: "Workspace settings", icon: }, - { label: "Invite team members", icon: }, - ].map((item) => ( - - ))} -
- - -
, - document.body, - ); -} - -export default function SidebarNav({ - activeTitle, - className = "", - fill = false, - onNewChat, - onPick, - activeNav, - onNavigate, - footerLabel = "Upgrade", - footerIcon, - onFooterClick, - recents = DEFAULT_RECENTS, -}: SidebarNavProps) { - const [collapsed, setCollapsed] = useState(false); - const [internalNav, setInternalNav] = useState("chats"); - const currentNav = activeNav ?? internalNav; - const selectNav = (key: string) => { - setInternalNav(key); - onNavigate?.(key); - }; - const [demoActiveTitle, setDemoActiveTitle] = useState(null); - const [workspaceOpen, setWorkspaceOpen] = useState(false); - const [workspacePosition, setWorkspacePosition] = useState({ top: 0, left: 0 }); - const [searchOpen, setSearchOpen] = useState(false); - const [query, setQuery] = useState(""); - const workspaceButtonRef = useRef(null); - const searchRef = useRef(null); - - const selectedTitle = activeTitle === undefined ? demoActiveTitle : activeTitle; - const visibleRecents = recents.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase())); - - useEffect(() => { - if (!workspaceOpen) return; - const close = (event: PointerEvent) => { - const target = event.target as Element; - if (!target.closest("[data-workspace-trigger]") && !target.closest("[data-workspace-menu]")) { - setWorkspaceOpen(false); - } - }; - document.addEventListener("pointerdown", close); - return () => document.removeEventListener("pointerdown", close); - }, [workspaceOpen]); - - useEffect(() => { - if (searchOpen) searchRef.current?.focus(); - }, [searchOpen]); - - const collapse = () => { - setCollapsed(true); - setWorkspaceOpen(false); - setSearchOpen(false); - setQuery(""); - }; - - return ( - - ); -} diff --git a/scripts/beautifului/stage/StreamingText.tsx b/scripts/beautifului/stage/StreamingText.tsx deleted file mode 100644 index 46b368d..0000000 --- a/scripts/beautifului/stage/StreamingText.tsx +++ /dev/null @@ -1,252 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -/* ───────────────────────────────────────────────────────── - * STREAMING TEXT - * Words resolve out of blur, inline citations appear in - * context, then actions and follow-up prompts become usable. - * ───────────────────────────────────────────────────────── */ - -const WORD_MS = 55; -const HOLD_MS = 3400; - -/* one streamed word, or a `cite` placeholder that renders an inline source chip */ -export type StreamingToken = { text: string; cite?: boolean }; - -const TOKENS: StreamingToken[] = [ - ..."Pistachio is your fastest-growing flavor — sales are up 23% this month and margins beat vanilla by 8 points." - .split(" ") - .map((text) => ({ text })), - { text: "", cite: true }, - ..."Stone-fruit flavors are trending in the same range." - .split(" ") - .map((text) => ({ text })), -]; - -const FOLLOW_UPS = [ - "Which flavors sell best in winter", - "Compare gelato and soft serve margins", -]; - -const SOURCE_IMAGES = { - scoop: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%231f7a5f'/%3E%3Cpath d='M20 36c0 7 5.4 12 12 12s12-5 12-12H20Z' fill='%23fff'/%3E%3Ccircle cx='32' cy='25' r='11' fill='%23bff3dd'/%3E%3Cpath d='M24 24c4-7 13-7 17 0' fill='none' stroke='%231f7a5f' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E", - trends: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%232f6fec'/%3E%3Cpath d='M15 43 27 31l8 7 14-18' fill='none' stroke='%23fff' stroke-width='7' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='49' cy='20' r='5' fill='%23bfe0ff'/%3E%3C/svg%3E", - market: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23e56d24'/%3E%3Cpath d='M17 45V25h8v20h-8Zm11 0V16h8v29h-8Zm11 0V30h8v15h-8Z' fill='%23fff'/%3E%3Cpath d='M16 49h32' stroke='%23ffd6b8' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E", -}; - -/* one cited source rendered as an inline chip and in the sources list */ -export type StreamingSource = { name: string; domain: string; href: string; image: string }; - -const SOURCES: StreamingSource[] = [ - { name: "Scoop Data", domain: "scoopdata.io", href: "https://scoopdata.io/", image: SOURCE_IMAGES.scoop }, - { name: "Trends Index", domain: "trends.google.com", href: "https://trends.google.com/trends/", image: SOURCE_IMAGES.trends }, - { name: "Market Basket", domain: "marketbasket.io", href: "https://marketbasket.io/", image: SOURCE_IMAGES.market }, -]; - -function sourceImage(source: StreamingSource) { - return source.image; -} - -function SourceChip({ source }: { source?: StreamingSource }) { - if (!source) return null; - return ( - - - {source.domain} - - ); -} - -const ACTION_ICONS: React.ReactNode[] = [ - , - , - , - , -]; - -export type StreamingLabels = { - /** label on the collapsed sources toggle */ - sources: string; - /** heading above the follow-up prompts */ - followUps: string; -}; - -const DEFAULT_LABELS: StreamingLabels = { - sources: "10 sources", - followUps: "Follow-ups", -}; - -export default function StreamingText({ - content = TOKENS, - sources = SOURCES, - followUps = FOLLOW_UPS, - labels, - loop = true, - fill = false, - onDone, - onFollowUp, -}: { - variant?: string; - /** the streamed tokens; `cite` tokens render an inline source chip */ - content?: StreamingToken[]; - /** cited sources shown in the chip, avatar stack, and expanded list */ - sources?: StreamingSource[]; - /** follow-up prompt suggestions shown once the stream completes */ - followUps?: string[]; - /** prominent copy strings */ - labels?: Partial; - /** restart the stream after a hold; turn off when embedding in a real thread */ - loop?: boolean; - /** fill the parent width instead of the gallery's fixed measure */ - fill?: boolean; - onDone?: () => void; - /** fired when a follow-up prompt is chosen */ - onFollowUp?: (text: string, index: number) => void; -} = {}) { - const l = { ...DEFAULT_LABELS, ...labels }; - const [count, setCount] = useState(0); - const [sourcesOpen, setSourcesOpen] = useState(false); - const done = count >= content.length; - - useEffect(() => { - if (done && !loop) { - onDone?.(); - return; - } - const t = setTimeout( - () => setCount((c) => (c >= content.length ? 0 : c + 1)), - done ? HOLD_MS : WORD_MS, - ); - return () => clearTimeout(t); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [count, done, loop]); - - return ( -
-

- {content.slice(0, count).map((token, i) => - token.cite ? ( - - ) : ( - - {token.text}{" "} - - ), - )} - {!done && ( - - )} -

- - {/* action icons row */} -
- {ACTION_ICONS.map((icon, i) => ( - - ))} - -
- -
-
-
- {sources.map((source) => ( - - - {source.name} - {source.domain} - - ))} -
-
-
- - {/* follow-ups */} -
-

{l.followUps}

-
- {followUps.map((text, i) => ( - - ))} -
-
-
- ); -} diff --git a/scripts/beautifului/stage/ThinkingState.tsx b/scripts/beautifului/stage/ThinkingState.tsx deleted file mode 100644 index d5c7f74..0000000 --- a/scripts/beautifului/stage/ThinkingState.tsx +++ /dev/null @@ -1,273 +0,0 @@ -"use client"; - -import { useEffect, useLayoutEffect, useRef, useState } from "react"; - -/* ───────────────────────────────────────────────────────── - * THINKING — expandable agent trace, four variants - * - * Steps step list with spinner → muted checks - * Reasoning prose reasoning that expands, then settles - * Search web-search trace: query + sources read - * Coding tool trace: files read, edits, commands - * - * The trace runs once, settles, and remains expandable. - * ───────────────────────────────────────────────────────── */ - -const STAGES = [800, 600, 1800, 2600, 1600]; - -function useSequence(steps: number[]) { - const [stage, setStage] = useState(0); - useEffect(() => { - if (stage >= steps.length - 1) return; - const t = setTimeout(() => setStage((s) => s + 1), steps[stage]); - return () => clearTimeout(t); - }, [stage, steps]); - return stage; -} - -type Row = { - primary: string; - secondary?: string; - mono?: boolean; - add?: number; - del?: number; - href?: string; -}; - -const VARIANTS: Record< - string, - { active: string; done: string; rows: Row[]; query?: string } -> = { - Steps: { - active: "Thinking", - done: "Thought for 4 seconds", - rows: [ - { primary: "Reading flavor briefs" }, - { primary: "Scanning supplier lists" }, - { primary: "Comparing tasting notes", secondary: "6 flavors" }, - { primary: "Writing the scoop report" }, - ], - }, - Reasoning: { - active: "Thinking", - done: "Thought for 4 seconds", - rows: [ - { primary: "Summer demand spikes for stone-fruit flavors — peach and apricot lead." }, - { primary: "I should check cone inventory before promoting a waffle-bowl special." }, - ], - }, - Search: { - active: "Searching the web", - done: "Searched the web", - query: "best waffle cone supplier", - rows: [ - { primary: "Joy Cone", secondary: "joycone.com", href: "https://joycone.com/fs_products/waffle-cones/" }, - { primary: "WebstaurantStore", secondary: "webstaurantstore.com", href: "https://www.webstaurantstore.com/ice-cream-shop-supplies.html" }, - { primary: "The Konery", secondary: "thekonery.com", href: "https://www.thekonery.com/" }, - ], - }, - Coding: { - active: "Running tools", - done: "Ran 3 tools", - rows: [ - { primary: "Read", secondary: "flavors.ts", mono: true }, - { primary: "Edit", secondary: "ChurnSchedule.tsx", mono: true, add: 74, del: 41 }, - { primary: "Run", secondary: "npm run freeze", mono: true }, - ], - }, -}; - -function Dot({ tone }: { tone: string }) { - return ( - - - - - - - ); -} - -const TONES = ["bg-accent", "bg-orange", "bg-green"]; - -export default function ThinkingState({ variant = "Steps", onSettled }: { variant?: string; onSettled?: () => void }) { - const stage = useSequence(STAGES); - const [manualExpanded, setManualExpanded] = useState(null); - const [selectedTool, setSelectedTool] = useState(null); - const v = VARIANTS[variant] ?? VARIANTS.Steps; - const autoExpanded = stage >= 1 && stage < 4; - const expanded = manualExpanded ?? autoExpanded; - const working = stage < 3; - const visible = stage < 2 ? 0 : stage === 2 ? Math.min(2, v.rows.length) : v.rows.length; - const traceRef = useRef(null); - const [lineHeight, setLineHeight] = useState(0); - useLayoutEffect(() => { - if (traceRef.current) setLineHeight(traceRef.current.offsetHeight); - }, [visible, expanded, variant, stage]); - - /* let embedders sequence content after the trace settles */ - const settledRef = useRef(false); - useEffect(() => { - if (working || settledRef.current) return; - settledRef.current = true; - onSettled?.(); - }, [working, onSettled]); - - return ( -
- {/* header — shared across variants */} - - - {/* expandable trace */} -
-
-
- -
- {v.query && ( -
- - - - - {v.query} -
- )} - {v.rows.slice(0, visible).map((row, i) => { - const content = ( - <> - {variant === "Search" && } - {variant === "Steps" && ( - i < visible - 1 || !working ? ( - - - - ) : ( - - ) - )} - - {row.primary} - - {row.secondary && ( - - {row.secondary} - - )} - {row.add !== undefined && ( - - +{row.add}{" "} - −{row.del} - - )} - - ); - const rowClass = "flex min-h-7 w-full items-center gap-2 rounded-[6px] px-1.5 py-0.5 text-left"; - const animation = { animation: `fade-up 320ms cubic-bezier(0.23,1,0.32,1) ${i * 120}ms both` }; - - if (variant === "Search") { - return ( - - {content} - - ); - } - - if (variant === "Coding") { - const selected = selectedTool === row.primary; - return ( - - ); - } - - return ( -
- {content} -
- ); - })} - {variant === "Search" && stage >= 3 && ( - - +7 more - - )} -
-
-
-
-
- ); -} diff --git a/scripts/beautifului/stage/ToolChips.tsx b/scripts/beautifului/stage/ToolChips.tsx deleted file mode 100644 index 5af8805..0000000 --- a/scripts/beautifului/stage/ToolChips.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; - -/* ───────────────────────────────────────────────────────── - * TOOL CHIPS - * An agent run as compact rows: tool calls with inline - * chips, then file-diff chips summarizing the edits. - * Hover a row to reveal its chevron; every row expands - * to show what the tool actually did. - * ───────────────────────────────────────────────────────── */ - -const STEP_MS = 700; - -const Icons: Record = { - think: , - write: , - run: , - read: , -}; - -export type ToolDetailLine = { text: string; tone?: "add" }; - -export type ToolStep = { - icon: string; - label: string; - chip: string; - mono: boolean; - detailMono: boolean; - detail: ToolDetailLine[]; -}; - -export type ToolDiff = { file: string; add: number; del: number }; - -export type ToolDiffLine = { text: string; tone: "add" | "del" | "ctx" }; - -export type ToolChipsLabels = { - header: string; - more: string; -}; - -const DEFAULT_LABELS: ToolChipsLabels = { - header: "4 tool calls, 2 messages", - more: "+2 more", -}; - -const ROWS: ToolStep[] = [ - { - icon: "think", label: "Thinking", chip: "Planning the churn schedule…", mono: false, detailMono: false, - detail: [ - { text: "Weekend demand carries pistachio, so it churns first." }, - { text: "Batch capacity leaves two evening freezer windows." }, - ], - }, - { - icon: "write", label: "Write 204 lines", chip: "ChurnSchedule.tsx", mono: true, detailMono: true, - detail: [ - { text: "+ const windows = slots.filter((s) => s.temp <= -12)", tone: "add" }, - { text: "+ return schedule(windows, { hero: \"pistachio\" })", tone: "add" }, - ], - }, - { - icon: "run", label: "Rebuild and verify", chip: "npm run freeze", mono: true, detailMono: true, - detail: [ - { text: "✓ built in 1.2s" }, - { text: "✓ 34 checks passed" }, - ], - }, - { - icon: "read", label: "Read image", chip: "flavor-chart.png", mono: true, detailMono: false, - detail: [ - { text: "1280 × 720 · line chart, three summers." }, - { text: "Mint chip trends up 12% through July." }, - ], - }, -]; - -const DIFFS: ToolDiff[] = [ - { file: "flavors.css", add: 13, del: 0 }, - { file: "ChurnSchedule.tsx", add: 74, del: 41 }, - { file: "menu.ts", add: 8, del: 2 }, -]; - -/* hovering a file chip opens its diff — green added, red removed */ -const DIFF_LINES: Record = { - "flavors.css": [ - { text: ".scoop-card {", tone: "ctx" }, - { text: " gap: 14px;", tone: "del" }, - { text: " gap: 12px;", tone: "add" }, - { text: " container-type: inline-size;", tone: "add" }, - { text: "}", tone: "ctx" }, - ], - "ChurnSchedule.tsx": [ - { text: "const slots = coldSlots(week);", tone: "ctx" }, - { text: "const windows = slots;", tone: "del" }, - { text: "const windows = slots.filter(", tone: "add" }, - { text: " (s) => s.temp <= -12,", tone: "add" }, - { text: ");", tone: "add" }, - ], - "menu.ts": [ - { text: "export const hero = \"mint-chip\";", tone: "del" }, - { text: "export const hero = \"pistachio\";", tone: "add" }, - ], -}; - -export default function ToolChips({ - steps = ROWS, - diffs = DIFFS, - diffLines = DIFF_LINES, - labels, - className, - onOpenChange, - onToggleRow, -}: { - /** Accepted for gallery/registry parity; ToolChips has no visual variants. */ - variant?: string; - steps?: ToolStep[]; - diffs?: ToolDiff[]; - diffLines?: Record; - labels?: Partial; - className?: string; - onOpenChange?: (open: boolean) => void; - onToggleRow?: (label: string, open: boolean) => void; -} = {}) { - const copy = { ...DEFAULT_LABELS, ...labels }; - const [step, setStep] = useState(0); - const [open, setOpen] = useState(true); - const [openRows, setOpenRows] = useState>(new Set()); - /* Rendered in a body portal so animated/translated reply wrappers cannot - * redefine the fixed-position coordinate system. */ - const [preview, setPreview] = useState<{ - file: string; - x: number; - top?: number; - bottom?: number; - } | null>(null); - const openPreview = (file: string) => (event: React.SyntheticEvent) => { - const rect = (event.currentTarget as Element).closest("[data-diffchip]")!.getBoundingClientRect(); - const previewHeight = 38 + (diffLines[file]?.length ?? 0) * 19; - const fitsBelow = rect.bottom + 6 + previewHeight <= window.innerHeight - 12; - setPreview({ - file, - x: Math.max(12, Math.min(rect.left, window.innerWidth - 300)), - ...(fitsBelow - ? { top: rect.bottom + 6 } - : { bottom: window.innerHeight - rect.top + 6 }), - }); - }; - const closePreview = (file: string) => () => - setPreview((current) => (current?.file === file ? null : current)); - const total = steps.length + 1; // rows, then diff chips - - useEffect(() => { - if (step >= total) return; - const t = setTimeout(() => setStep((s) => s + 1), STEP_MS); - return () => clearTimeout(t); - }, [step, total]); - - const toggleRow = (label: string) => - setOpenRows((current) => { - const next = new Set(current); - next.has(label) ? next.delete(label) : next.add(label); - onToggleRow?.(label, next.has(label)); - return next; - }); - - return ( -
- {/* collapsed run header */} - - - {/* tool call rows */} -
- {/* -mx-1 + px-1.5 keeps content at the same x while giving the - row hover pills room inside this overflow-hidden clip box */} -
-
- {steps.slice(0, step).map((row) => { - const rowOpen = openRows.has(row.label); - return ( -
- - - {/* expanded detail */} -
-
-
- {row.detail.map((line) => ( - - {line.text} - - ))} -
-
-
-
- ); - })} -
- - {/* file-diff chips */} - {step >= total && ( -
- {diffs.map((d, i) => ( - - - - - ))} - -
- )} -
-
- {preview && typeof document !== "undefined" && createPortal( -
-
- {preview.file} - - +{diffs.find((diff) => diff.file === preview.file)?.add} - {(diffs.find((diff) => diff.file === preview.file)?.del ?? 0) > 0 && ( - −{diffs.find((diff) => diff.file === preview.file)?.del} - )} - -
-
- {(diffLines[preview.file] ?? []).map((line, index) => ( -
- {line.tone === "add" ? "+" : line.tone === "del" ? "−" : " "} - {line.text} -
- ))} -
-
, - document.body, - )} -
- ); -} diff --git a/scripts/beautifului/stage/foundation.css b/scripts/beautifului/stage/foundation.css deleted file mode 100644 index b4a38bc..0000000 --- a/scripts/beautifului/stage/foundation.css +++ /dev/null @@ -1,643 +0,0 @@ -@import "tailwindcss"; -/* shadow-plugin (flornkm) — smooth, layered shadow stacks. The unprefixed - * entry maps them onto Tailwind's --shadow-xs…2xl scale; our semantic tokens - * below build on those values so the whole app picks up the smooth shadows. */ -@import "shadow-plugin/unprefixed"; - -@custom-variant dark (&:where(.dark, .dark *)); - -/* GENERIC FOUNDATION — tokens, @theme, base rules, the primitive-* - * spacing utilities, and shared keyframes below are needed by EVERY - * component. Blocks tagged COMPONENT-SPECIFIC further down belong to - * one primitive each. */ -/* ────────────────────────────────────────────────────────── - * Beautiful UI — design tokens - * Cool near-white canvas, white cards, hairline borders, layered - * single-digit-opacity shadows, a neutral ink ramp, and semantic - * color used sparingly as a condiment. - * ────────────────────────────────────────────────────────── */ - -:root { - /* surfaces — cool, blue-tinted neutrals */ - --page: oklch(0.985 0.001 286.376); - --canvas: oklch(0.961 0.002 247.84); - --surface: oklch(1 0 0); - --inset: oklch(0.979 0.002 247.839); - --hover: oklch(0.97 0.002 247.839); - --hover-2: oklch(0.933 0.003 247.86); - - /* ink ramp */ - --ink: oklch(0.247 0.006 258.361); - --ink-2: oklch(0.506 0.01 264.477); - --ink-3: oklch(0.695 0.009 264.505); - - /* borders — solid and crisp, not alpha */ - --line: oklch(0.946 0.003 264.542); - --line-strong: oklch(0.912 0.005 258.326); - --line-soft: oklch(0.966 0.002 264.542); - --grid-line: color-mix(in srgb, var(--line) 78%, transparent); - --field: oklch(0.961 0.001 286.375); - --stripe: oklch(0.405 0 0 / 0.075); - --stripe-bg: oklch(0.97 0 0); - - /* accent */ - --accent: oklch(0.626 0.205 254.947); - --accent-ink: oklch(0.556 0.187 255.617); - --accent-tint: oklch(0.96 0.019 252.878); - - /* semantic */ - --green: oklch(0.603 0.155 150.883); - --green-tint: oklch(0.958 0.017 159.118); - --orange: oklch(0.689 0.179 49.902); - --orange-tint: oklch(0.964 0.021 67.581); - --red: oklch(0.621 0.192 23.042); - --red-tint: oklch(0.956 0.017 17.462); - - /* chart overlays — neutral system colors with enough contrast on either theme */ - --tooltip-bg: oklch(0.272 0.008 264.435); - --tooltip-fg: oklch(0.976 0.002 247.839); - --tooltip-muted: oklch(0.731 0.008 260.731); - --tooltip-border: oklch(0.356 0.007 264.474); - - /* shadows — our solid hairline ring + shadow-plugin's smooth layered stacks - * (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads - * consistently across the app. */ - --shadow-hairline: 0 0 0 1px var(--line); - --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs); - --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm); - --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md); - --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg); - --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12); -} - -.dark { - --page: oklch(0.209 0.004 264.477); - --canvas: oklch(0.231 0.004 264.487); - --surface: oklch(0.26 0.006 271.191); - --inset: oklch(0.243 0.004 264.492); - --hover: oklch(0.289 0.006 271.22); - --hover-2: oklch(0.318 0.007 274.747); - - --ink: oklch(0.964 0.002 247.839); - --ink-2: oklch(0.731 0.008 260.731); - --ink-3: oklch(0.541 0.01 264.484); - - --line: oklch(0.308 0.006 258.354); - --line-strong: oklch(0.356 0.007 264.474); - --line-soft: oklch(0.278 0.006 258.354); - --field: oklch(0.293 0.006 271.223); - --stripe: oklch(1 0 0 / 0.055); - --stripe-bg: oklch(0.226 0.004 264.485); - - --accent: oklch(0.68 0.173 253.301); - --accent-ink: oklch(0.788 0.113 248.33); - --accent-tint: oklch(0.68 0.173 253.301 / 0.16); - - --green: oklch(0.705 0.154 153.814); - --green-tint: oklch(0.705 0.154 153.814 / 0.14); - --orange: oklch(0.746 0.156 55.642); - --orange-tint: oklch(0.746 0.156 55.642 / 0.14); - --red: oklch(0.666 0.18 21.433); - --red-tint: oklch(0.666 0.18 21.433 / 0.14); - - --tooltip-bg: oklch(0.182 0.004 264.459); - --tooltip-fg: oklch(0.964 0.002 247.839); - --tooltip-muted: oklch(0.731 0.008 260.731); - --tooltip-border: oklch(0.308 0.006 258.354); - - --shadow-hairline: 0 0 0 1px var(--line); - /* Elevation rings go to a subtle *light* hairline in dark mode: the ring - * paints over the surface behind the element, so a low-alpha white edge - * dissolves into the shadow as one continuous stroke (per smooth-shadow-ring), - * graduated by how far the surface floats. */ - --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3); - --shadow-card: - 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2), - 0 2px 6px oklch(0 0 0 / 0.2); - --shadow-raised: - 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22); - --shadow-overlay: - 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34); - --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4); -} - -@theme inline { - /* colors */ - --color-page: var(--page); - --color-canvas: var(--canvas); - --color-surface: var(--surface); - --color-inset: var(--inset); - --color-ink: var(--ink); - --color-ink-2: var(--ink-2); - --color-ink-3: var(--ink-3); - --color-hover: var(--hover); - --color-hover-2: var(--hover-2); - --color-field: var(--field); - --color-line: var(--line); - --color-line-strong: var(--line-strong); - --color-accent: var(--accent); - --color-accent-ink: var(--accent-ink); - --color-accent-tint: var(--accent-tint); - --color-green: var(--green); - --color-green-tint: var(--green-tint); - --color-orange: var(--orange); - --color-orange-tint: var(--orange-tint); - --color-red: var(--red); - --color-red-tint: var(--red-tint); - - /* shadows */ - --shadow-hairline: var(--shadow-hairline); - --shadow-btn: var(--shadow-btn); - --shadow-inset-field: var(--shadow-inset-field); - --shadow-card: var(--shadow-card); - --shadow-raised: var(--shadow-raised); - --shadow-overlay: var(--shadow-overlay); - - /* type */ - --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; - --font-mono: var(--font-mono-face), ui-monospace, "SF Mono", monospace; - - /* radii — chips 6, controls 8, cards 10, windows 14, pills 999 */ - --radius-chip: 6px; - --radius-control: 8px; - --radius-card: 10px; - --radius-window: 14px; - - /* easing — strong curves; built-ins are too weak */ - --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1); - --ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1); - --ease-link: cubic-bezier(0.16, 1, 0.3, 1); -} - -/* ── base ─────────────────────────────────────────────── */ - -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; -} - -body { - background-color: var(--stripe-bg); - background-image: repeating-linear-gradient( - -45deg, - transparent 0, - transparent 7px, - var(--stripe) 7px, - var(--stripe) 8px - ); - /* Pin the stripe pattern to the viewport so it stays put on scroll. */ - background-attachment: fixed; - color: var(--ink); - font-feature-settings: "cv11", "ss01"; - font-size: 14px; - letter-spacing: -0.01em; - line-height: 1.5; -} - -::selection { - background: var(--accent-tint); - color: var(--ink); -} - -/* ── interface hygiene (interfaces.dev cheat sheet) ────── */ - -/* headings balance; descriptions avoid orphans (skip long-form — we have none) */ -h1, -h2, -h3, -h4 { - text-wrap: balance; -} - -p { - text-wrap: pretty; -} - -/* underlines clear descenders */ -a { - text-underline-position: from-font; - text-decoration-skip-ink: auto; -} - -/* theme switches swap every token at once — freeze transitions so the flip - * is one clean repaint instead of hundreds of mismatched color fades */ -.theme-switching *, -.theme-switching *::before, -.theme-switching *::after { - transition: none !important; -} - -/* touch: :hover sticks after a tap, so neutralize row/header hovers there */ -@media (hover: none) { - .records-row:hover > .records-cell { - background: transparent; - } - - .records-row:hover > td.is-colsel { - background: color-mix(in srgb, var(--accent) 4%, var(--surface)); - } - - .records-header-button:hover { - background: transparent; - color: var(--ink-2); - } -} - -/* Keep the component nav scrollable but hide its scrollbar. */ -.component-nav-scroll { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* legacy Edge */ -} -.component-nav-scroll::-webkit-scrollbar { - display: none; /* WebKit */ -} - -@media (min-width: 64rem) { - .component-nav-scroll { - -webkit-mask-image: linear-gradient( - to bottom, - oklch(0 0 0) 0, - oklch(0 0 0) calc(100% - 6rem), - transparent 100% - ); - mask-image: linear-gradient( - to bottom, - oklch(0 0 0) 0, - oklch(0 0 0) calc(100% - 6rem), - transparent 100% - ); - } -} - -/* Mobile demo gestures should operate controls and visualizations rather than - * selecting surrounding labels. Text entry and code remain intentionally - * selectable. */ -@media (hover: none) and (pointer: coarse) { - .primitive-showcase { - -webkit-user-select: none; - user-select: none; - -webkit-touch-callout: none; - } - - .primitive-showcase :is(input, textarea, [contenteditable="true"], pre, code) { - -webkit-user-select: text; - user-select: text; - -webkit-touch-callout: default; - } - - .primitive-showcase :is(a, button, [role="button"]) { - -webkit-tap-highlight-color: transparent; - } - - .primitive-showcase :is(a, button, [role="button"]):focus-visible { - outline: none; - } - - .primitive-showcase :is(img, svg, canvas) { - -webkit-user-drag: none; - } - - .insight-chart-stage { - touch-action: pan-y; - } -} - -/* Tailwind v4 resets buttons to cursor:default — anything - * clickable must invite the click */ -button:not(:disabled), -[role="button"]:not([aria-disabled="true"]), -label:has(input), -a { - cursor: pointer; -} - - -:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - border-radius: 4px; -} - -input:focus-visible, -textarea:focus-visible, -select:focus-visible, -[contenteditable]:focus-visible { - outline: none; -} - -.animated-underline { - position: relative; - display: inline-block; -} - -.animated-underline::after { - position: absolute; - right: 0; - bottom: -1px; - left: 0; - height: 1px; - content: ""; - background: currentColor; - transform: scaleX(0); - transform-origin: left center; - transition: transform 280ms var(--ease-link); -} - -a:hover .animated-underline::after, -a:focus-visible .animated-underline::after { - transform: scaleX(1); -} - -/* ══════════════════════════════════════════════════════════ - * COMPONENT-SPECIFIC — StreamingText / ContextCards - * Favicon source avatars. - * Copy this block only alongside StreamingText / ContextCards; it is not part of the - * generic foundation above. - * ══════════════════════════════════════════════════════════ */ -.source-avatar { - box-shadow: 0 0 0 1px oklch(0.21 0.034 263.436 / 0.1); -} - -.dark .source-avatar { - box-shadow: 0 0 0 1px oklch(1 0 0 / 0.08); -} - -/* ── primitive spacing system ───────────────────────────── - * Shared optical offsets for small demo cards. Use these - * instead of hand-tuning 10/12/14px per component. - */ - -.primitive-card-pad { - padding: 12px; -} - -.primitive-card-bar { - padding: 10px 12px; -} - -.primitive-card-footer { - padding: 10px; -} - -.primitive-table-cell { - padding: 10px 12px; -} - -.primitive-icon-button { - display: inline-flex; - width: 28px; - height: 28px; - align-items: center; - justify-content: center; - border-radius: var(--radius-control); -} - -/* ── shared keyframes ─────────────────────────────────── */ - -@keyframes shimmer-text { - from { - background-position: 150% center; - } - to { - background-position: -50% center; - } -} - -@keyframes fade-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.home-reveal { - will-change: opacity, transform, filter; -} - -/* The sidebar is one persistent 224px tree clipped by a 52px shell. - * Every rail glyph remains centered at x=26 in both states. */ - .records-footer-hint { - display: none; - } -} - -/* ── filter table electric status colors ─────────────────── */ - -/* ══════════════════════════════════════════════════════════ - * COMPONENT-SPECIFIC — FilterTable - * Electric status-pill colors. - * Copy this block only alongside FilterTable; it is not part of the - * generic foundation above. - * ══════════════════════════════════════════════════════════ */ -.filter-status-todo, -.filter-status-progress, -.filter-status-done { - border: 1px solid transparent; -} - -/* ══════════════════════════════════════════════════════════ - * COMPONENT-SPECIFIC — InsightCards - * Liveline chart tooltip + cursor. - * Copy this block only alongside InsightCards; it is not part of the - * generic foundation above. - * ══════════════════════════════════════════════════════════ */ -.insight-chart-stage { - overflow: hidden; -} - -.insight-chart-cursor { - position: absolute; - z-index: 4; - top: 0; - bottom: 0; - width: 1px; - background: var(--ink); - opacity: 0.26; - pointer-events: none; -} - -.insight-chart-tooltip-anchor { - position: absolute; - z-index: 5; - top: 8px; - transform: translateX(-50%); - pointer-events: none; -} - -.insight-chart-tooltip { - display: flex; - align-items: center; - gap: 12px; - padding: 5px 9px; - border: 1px solid var(--line-strong); - border-radius: 8px; - color: var(--tooltip-fg); - background: var(--tooltip-bg); - box-shadow: var(--shadow-overlay); - font-size: 11.5px; - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.insight-chart-tooltip-item { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.insight-chart-tooltip-dot { - width: 8px; - height: 8px; - flex: 0 0 8px; - border-radius: 50%; -} - -/* Status pills share the same base-hue → color-mix derivation as .records-tag, - * so both components stay consistent across light and dark. */ -.filter-status-todo { - --tag-base: oklch(0.757 0.153 66.401); -} -.filter-status-progress { - --tag-base: oklch(0.671 0.118 219.351); -} -.filter-status-done { - --tag-base: oklch(0.652 0.131 162.865); -} - -.filter-status-todo, -.filter-status-progress, -.filter-status-done { - color: color-mix(in srgb, var(--tag-base) 92%, var(--ink)); - background: color-mix(in srgb, var(--tag-base) 20%, var(--surface)); - border-color: color-mix(in srgb, var(--tag-base) 34%, var(--surface)); -} - -/* Dark mode — a richer, saturated same-hue fill - * with bright same-hue text and no visible border (light mode keeps the soft - * tinted pastels above). Applies to both the records grid and status pills. */ -.dark .records-tag, -.dark .filter-status-todo, -.dark .filter-status-progress, -.dark .filter-status-done { - color: color-mix(in srgb, var(--tag-base) 86%, white); - background: color-mix(in srgb, var(--tag-base) 34%, var(--surface)); - border-color: color-mix(in srgb, var(--tag-base) 34%, var(--surface)); -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes eq-bounce { - 0%, - 100% { - transform: scaleY(0.35); - } - 50% { - transform: scaleY(1); - } -} - -/* ── StreamText primitive ───────────────────────────────── - * Fast character reveal: the newest few characters carry a - * soft, masked blur so the leading edge resolves smoothly as - * text flows in. The caret holds solid while streaming, then - * blinks once the string settles. */ -/* ══════════════════════════════════════════════════════════ - * COMPONENT-SPECIFIC — StreamText - * The streaming caret + tail. - * Copy this block only alongside StreamText; it is not part of the - * generic foundation above. - * ══════════════════════════════════════════════════════════ */ -.stream-tail { - filter: blur(1.6px); - -webkit-mask-image: linear-gradient(to right, oklch(0 0 0) 20%, oklch(0 0 0 / 0.2)); - mask-image: linear-gradient(to right, oklch(0 0 0) 20%, oklch(0 0 0 / 0.2)); -} - -.stream-caret { - display: inline-block; - width: 2px; - height: 1.05em; - margin-left: 1.5px; - translate: 0 -0.5px; - border-radius: 1px; - background: var(--ink); - vertical-align: text-bottom; - animation: caret-blink 1s step-end infinite; -} - -.stream-caret.is-streaming { - animation: none; -} - -@keyframes caret-blink { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0; - } -} - -@media (prefers-reduced-motion: reduce) { - .stream-caret { - animation: none; - } - .stream-tail { - filter: none; - -webkit-mask-image: none; - mask-image: none; - } -} - -@keyframes pop-in { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* pixel-grid loader — brighten plateau, staggered per cell */ -@keyframes pixel-on { - 0%, 100% { - opacity: 0.15; - } - 18%, 42% { - opacity: 1; - } - 62% { - opacity: 0.15; - } -} - -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/scripts/beautifului/stage/sidebar-nav.css b/scripts/beautifului/stage/sidebar-nav.css deleted file mode 100644 index 8977dc0..0000000 --- a/scripts/beautifului/stage/sidebar-nav.css +++ /dev/null @@ -1,81 +0,0 @@ -/* ══════════════════════════════════════════════════════════ - * COMPONENT-SPECIFIC — SidebarNav - * The 52↔224px collapsible sidebar tree. - * Copy this block only alongside SidebarNav; it is not part of the - * generic foundation above. - * ══════════════════════════════════════════════════════════ */ -.sidebar-row { - width: 208px; - transition-duration: var(--sidebar-copy-duration); - transition-timing-function: var(--sidebar-easing); -} - -.sidebar-copy { - opacity: 1; - transform: translateX(0); - transition: - opacity var(--sidebar-copy-duration) ease-out, - transform var(--sidebar-copy-duration) var(--sidebar-easing); -} - -.sidebar-logo, -.sidebar-collapse-control, -.sidebar-expand-control { - transition-duration: var(--sidebar-copy-duration); - transition-timing-function: ease-out; -} - -.sidebar-expand-control { - pointer-events: none; - opacity: 0; -} - -.sidebar-glide-highlight { - right: 8px; - left: 8px; -} - -[data-sidebar-collapsed="true"] .sidebar-row { - width: 36px; -} - -[data-sidebar-collapsed="true"] .sidebar-copy { - pointer-events: none; - opacity: 0; - transform: translateX(calc(var(--sidebar-copy-offset) * -1)); -} - -[data-sidebar-collapsed="true"] .sidebar-workspace-control { - pointer-events: none; -} - -[data-sidebar-collapsed="true"] .sidebar-logo, -[data-sidebar-collapsed="true"] .sidebar-collapse-control { - pointer-events: none; - opacity: 0; -} - -[data-sidebar-collapsed="true"] .sidebar-expand-control { - pointer-events: auto; - opacity: 1; -} - -[data-sidebar-collapsed="true"] .sidebar-glide-highlight { - right: auto; - width: 36px; -} - -@media (prefers-reduced-motion: reduce) { - .home-reveal { - opacity: 1 !important; - transform: none !important; - filter: none !important; - } -} - -/* ── records table ──────────────────────────────────────── - * The grid is deliberately more architectural than card-like: - * one quiet surface, precise rules, and color reserved for - * meaning. The first column remains an anchor while both axes - * can move independently inside the table viewport. - */ diff --git a/scripts/beautifului/stream-text.json b/scripts/beautifului/stream-text.json deleted file mode 100644 index 68a6b0e..0000000 --- a/scripts/beautifului/stream-text.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "stream-text", - "type": "registry:ui", - "title": "StreamText", - "description": "Shared building block used by other Beautiful UI components.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/atoms/StreamText.tsx", - "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * STREAM TEXT — reusable streaming primitive\n * Reveals characters quickly (fast, like a real token stream);\n * the leading edge resolves out of a soft blur, and the caret\n * stays solid while streaming, then blinks once the text\n * settles. Inherits typography from its context, so it drops\n * into any text surface (Selection Actions, chat, etc.).\n * ───────────────────────────────────────────────────────── */\n\nexport function StreamText({\n text,\n charsPerTick = 2,\n tickMs = 9,\n blurTail = 6,\n caret = true,\n className,\n onProgress,\n onDone,\n}: {\n text: string;\n /** characters revealed per tick — higher is faster */\n charsPerTick?: number;\n /** interval between reveals, ms */\n tickMs?: number;\n /** how many trailing characters carry the soft blur edge */\n blurTail?: number;\n /** render the caret (solid while streaming, blinks once idle) */\n caret?: boolean;\n className?: string;\n /** fires each tick — useful for re-anchoring UI to reflowing text */\n onProgress?: () => void;\n /** fires once the full string is shown */\n onDone?: () => void;\n}) {\n const [count, setCount] = useState(0);\n const onProgressRef = useRef(onProgress);\n const onDoneRef = useRef(onDone);\n onProgressRef.current = onProgress;\n onDoneRef.current = onDone;\n\n useEffect(() => {\n setCount(0);\n let i = 0;\n const id = setInterval(() => {\n i = Math.min(i + charsPerTick, text.length);\n setCount(i);\n onProgressRef.current?.();\n if (i >= text.length) {\n clearInterval(id);\n onDoneRef.current?.();\n }\n }, tickMs);\n return () => clearInterval(id);\n }, [text, charsPerTick, tickMs]);\n\n const streaming = count < text.length;\n const shown = text.slice(0, count);\n const split = streaming ? Math.max(0, shown.length - blurTail) : shown.length;\n\n return (\n \n {shown.slice(0, split)}\n {split < shown.length && (\n {shown.slice(split)}\n )}\n {caret && (\n \n )}\n \n );\n}\n", - "type": "registry:ui", - "target": "components/atoms/StreamText.tsx" - } - ] -} diff --git a/scripts/beautifului/streaming-text.json b/scripts/beautifului/streaming-text.json deleted file mode 100644 index 285548b..0000000 --- a/scripts/beautifului/streaming-text.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "streaming-text", - "type": "registry:component", - "title": "Streaming Text", - "description": "Streamed answer with inline sources, actions, and follow-ups.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/primitives/StreamingText.tsx", - "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * STREAMING TEXT\n * Words resolve out of blur, inline citations appear in\n * context, then actions and follow-up prompts become usable.\n * ───────────────────────────────────────────────────────── */\n\nconst WORD_MS = 55;\nconst HOLD_MS = 3400;\n\n/* one streamed word, or a `cite` placeholder that renders an inline source chip */\nexport type StreamingToken = { text: string; cite?: boolean };\n\nconst TOKENS: StreamingToken[] = [\n ...\"Pistachio is your fastest-growing flavor — sales are up 23% this month and margins beat vanilla by 8 points.\"\n .split(\" \")\n .map((text) => ({ text })),\n { text: \"\", cite: true },\n ...\"Stone-fruit flavors are trending in the same range.\"\n .split(\" \")\n .map((text) => ({ text })),\n];\n\nconst FOLLOW_UPS = [\n \"Which flavors sell best in winter\",\n \"Compare gelato and soft serve margins\",\n];\n\nconst SOURCE_IMAGES = {\n scoop:\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%231f7a5f'/%3E%3Cpath d='M20 36c0 7 5.4 12 12 12s12-5 12-12H20Z' fill='%23fff'/%3E%3Ccircle cx='32' cy='25' r='11' fill='%23bff3dd'/%3E%3Cpath d='M24 24c4-7 13-7 17 0' fill='none' stroke='%231f7a5f' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E\",\n trends:\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%232f6fec'/%3E%3Cpath d='M15 43 27 31l8 7 14-18' fill='none' stroke='%23fff' stroke-width='7' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='49' cy='20' r='5' fill='%23bfe0ff'/%3E%3C/svg%3E\",\n market:\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23e56d24'/%3E%3Cpath d='M17 45V25h8v20h-8Zm11 0V16h8v29h-8Zm11 0V30h8v15h-8Z' fill='%23fff'/%3E%3Cpath d='M16 49h32' stroke='%23ffd6b8' stroke-width='4' stroke-linecap='round'/%3E%3C/svg%3E\",\n};\n\n/* one cited source rendered as an inline chip and in the sources list */\nexport type StreamingSource = { name: string; domain: string; href: string; image: string };\n\nconst SOURCES: StreamingSource[] = [\n { name: \"Scoop Data\", domain: \"scoopdata.io\", href: \"https://scoopdata.io/\", image: SOURCE_IMAGES.scoop },\n { name: \"Trends Index\", domain: \"trends.google.com\", href: \"https://trends.google.com/trends/\", image: SOURCE_IMAGES.trends },\n { name: \"Market Basket\", domain: \"marketbasket.io\", href: \"https://marketbasket.io/\", image: SOURCE_IMAGES.market },\n];\n\nfunction sourceImage(source: StreamingSource) {\n return source.image;\n}\n\nfunction SourceChip({ source }: { source?: StreamingSource }) {\n if (!source) return null;\n return (\n \n \"\"\n {source.domain}\n \n );\n}\n\nconst ACTION_ICONS: React.ReactNode[] = [\n ,\n ,\n ,\n ,\n];\n\nexport type StreamingLabels = {\n /** label on the collapsed sources toggle */\n sources: string;\n /** heading above the follow-up prompts */\n followUps: string;\n};\n\nconst DEFAULT_LABELS: StreamingLabels = {\n sources: \"10 sources\",\n followUps: \"Follow-ups\",\n};\n\nexport default function StreamingText({\n content = TOKENS,\n sources = SOURCES,\n followUps = FOLLOW_UPS,\n labels,\n loop = true,\n fill = false,\n onDone,\n onFollowUp,\n}: {\n variant?: string;\n /** the streamed tokens; `cite` tokens render an inline source chip */\n content?: StreamingToken[];\n /** cited sources shown in the chip, avatar stack, and expanded list */\n sources?: StreamingSource[];\n /** follow-up prompt suggestions shown once the stream completes */\n followUps?: string[];\n /** prominent copy strings */\n labels?: Partial;\n /** restart the stream after a hold; turn off when embedding in a real thread */\n loop?: boolean;\n /** fill the parent width instead of the gallery's fixed measure */\n fill?: boolean;\n onDone?: () => void;\n /** fired when a follow-up prompt is chosen */\n onFollowUp?: (text: string, index: number) => void;\n} = {}) {\n const l = { ...DEFAULT_LABELS, ...labels };\n const [count, setCount] = useState(0);\n const [sourcesOpen, setSourcesOpen] = useState(false);\n const done = count >= content.length;\n\n useEffect(() => {\n if (done && !loop) {\n onDone?.();\n return;\n }\n const t = setTimeout(\n () => setCount((c) => (c >= content.length ? 0 : c + 1)),\n done ? HOLD_MS : WORD_MS,\n );\n return () => clearTimeout(t);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [count, done, loop]);\n\n return (\n
\n

\n {content.slice(0, count).map((token, i) =>\n token.cite ? (\n \n ) : (\n \n {token.text}{\" \"}\n \n ),\n )}\n {!done && (\n \n )}\n

\n\n {/* action icons row */}\n \n {ACTION_ICONS.map((icon, i) => (\n \n \n {icon}\n \n \n ))}\n setSourcesOpen((current) => !current)}\n className=\"ml-1.5 flex items-center gap-1.5 rounded-[6px] px-1 py-0.5 text-left transition-colors duration-150 hover:bg-hover\"\n >\n \n {sources.map((source) => (\n \n ))}\n \n {l.sources}\n \n
\n\n \n
\n
\n {sources.map((source) => (\n \n \"\"\n {source.name}\n {source.domain}\n \n ))}\n
\n
\n
\n\n {/* follow-ups */}\n \n

{l.followUps}

\n
\n {followUps.map((text, i) => (\n onFollowUp?.(text, i)}\n className=\"-mx-1.5 flex items-center gap-2 rounded-[7px] border-b border-line\n px-1.5 py-1.5 text-left text-[12.5px] text-ink transition-colors\n duration-100 hover:bg-hover-2\"\n style={\n done\n ? { animation: `fade-up 350ms cubic-bezier(0.23,1,0.32,1) ${i * 90}ms both` }\n : { opacity: 0 }\n }\n >\n \n \n \n \n {text}\n \n ))}\n
\n
\n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/StreamingText.tsx" - } - ] -} diff --git a/scripts/beautifului/task-rows.json b/scripts/beautifului/task-rows.json deleted file mode 100644 index fe44829..0000000 --- a/scripts/beautifului/task-rows.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "task-rows", - "type": "registry:component", - "title": "Task Rows", - "description": "Live agent task status — running, failed, completed.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/primitives/TaskRows.tsx", - "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * TASK ROWS\n *\n * 0ms rows enter staggered (80ms apart)\n * 600ms row 1 ring sweeps 0 → 66%\n * 1500ms row 1 expands — detail steps drop down\n * 3900ms row 1 collapses; row 2 flips to Failed + retry\n * 5300ms row 2 resolves to Completed\n * The status run completes once; task details stay clickable.\n * ───────────────────────────────────────────────────────── */\n\nconst TICKS = [600, 900, 2400, 1400, 2400, 600];\n\nfunction useTick(intervals: number[]) {\n const [tick, setTick] = useState(0);\n useEffect(() => {\n if (tick >= intervals.length - 1) return;\n const t = setTimeout(() => setTick((x) => x + 1), intervals[tick]);\n return () => clearTimeout(t);\n }, [tick, intervals]);\n return tick;\n}\n\nfunction SpinnerRing({ active, children }: { active?: boolean; children?: React.ReactNode }) {\n const size = 24, stroke = 2;\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n return (\n \n \n \n {active && (\n \n )}\n \n {children}\n \n );\n}\n\nfunction Badge({ tone, children }: { tone: \"red\" | \"green\"; children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\nconst XIcon = (\n \n);\nconst CheckIcon = (\n \n);\nconst RetryIcon = (\n \n);\n\n/* One detail line shown when a task row is expanded. */\nexport type TaskDetail = { label: string; meta: string };\n\n/* A single task row.\n * - \"done\" → green check badge + completed pill (static)\n * - \"running\" → active spinner showing `step`, no pill (static)\n * - \"sequence\" → animation-driven: pending spinner → failed → completed\n */\nexport type TaskRow = {\n key: string;\n label: string;\n amount: string;\n status: \"done\" | \"running\" | \"sequence\";\n step?: number;\n details: TaskDetail[];\n};\n\nexport type TaskRowsLabels = {\n completed: string;\n failed: string;\n};\n\nconst DEFAULT_LABELS: TaskRowsLabels = {\n completed: \"Completed\",\n failed: \"Failed\",\n};\n\nconst TASK_ROWS: TaskRow[] = [\n {\n key: \"verify\",\n label: \"Verified vendor records\",\n amount: \"12 suppliers\",\n status: \"done\",\n details: [\n { label: \"Matched tax and contact IDs\", meta: \"12/12\" },\n { label: \"Flagged stale records\", meta: \"0\" },\n ],\n },\n {\n key: \"index\",\n label: \"Build reorder task list\",\n amount: \"7 SKUs\",\n status: \"running\",\n step: 2,\n details: [\n { label: \"Reading POS export\", meta: \"3 files\" },\n { label: \"Scoring stockout risk\", meta: \"68%\" },\n ],\n },\n {\n key: \"draft\",\n label: \"Draft supplier emails\",\n amount: \"2 messages\",\n status: \"sequence\",\n step: 3,\n details: [\n { label: \"Cone supplier follow-up\", meta: \"draft\" },\n { label: \"Pistachio reorder note\", meta: \"draft\" },\n ],\n },\n];\n\nexport default function TaskRows({\n variant = \"Capsules\",\n rows = TASK_ROWS,\n labels,\n className,\n onToggleRow,\n}: {\n variant?: string;\n rows?: TaskRow[];\n labels?: Partial;\n className?: string;\n onToggleRow?: (key: string, open: boolean) => void;\n}) {\n const tick = useTick(TICKS);\n const [manualOpen, setManualOpen] = useState>({});\n const row2: \"pending\" | \"failed\" | \"done\" = tick < 3 ? \"pending\" : tick === 3 ? \"failed\" : \"done\";\n const copy = { ...DEFAULT_LABELS, ...labels };\n\n const badgeFor = (row: TaskRow) => {\n if (row.status === \"done\") return {CheckIcon};\n if (row.status === \"running\") return {row.step};\n return row2 === \"pending\" ? (\n {row.step}\n ) : row2 === \"failed\" ? (\n {XIcon}\n ) : (\n {CheckIcon}\n );\n };\n\n const pillFor = (row: TaskRow) => {\n if (row.status === \"done\")\n return (\n \n {copy.completed}\n \n );\n if (row.status === \"running\") return null;\n return row2 === \"failed\" ? (\n \n {copy.failed} {RetryIcon}\n \n ) : row2 === \"done\" ? (\n \n {copy.completed}\n \n ) : null;\n };\n\n const list = variant === \"List\";\n return (\n \n {rows.map((row, i) => {\n const open = manualOpen[row.key] ?? (row.key === \"index\" && tick === 2);\n return (\n \n {\n setManualOpen((current) => ({ ...current, [row.key]: !open }));\n onToggleRow?.(row.key, !open);\n }}\n className=\"flex h-11 w-full items-center gap-2.5 px-2.5 text-left\"\n >\n \n {badgeFor(row)}\n \n \n {row.label}\n \n {row.amount}\n {pillFor(row)}\n \n \n \n \n \n \n\n {/* dropdown detail — same expandable grammar as Chain of Thought */}\n \n
\n
\n \n
\n {row.details.map((d, j) => (\n \n {d.label}\n \n {d.meta}\n \n
\n ))}\n
\n
\n \n \n \n );\n })}\n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/TaskRows.tsx" - } - ] -} diff --git a/scripts/beautifului/thinking-state.json b/scripts/beautifului/thinking-state.json deleted file mode 100644 index 980b5d8..0000000 --- a/scripts/beautifului/thinking-state.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "thinking-state", - "type": "registry:component", - "title": "Thinking", - "description": "Expandable traces — steps, reasoning, search, coding.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/primitives/ThinkingState.tsx", - "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * THINKING — expandable agent trace, four variants\n *\n * Steps step list with spinner → muted checks\n * Reasoning prose reasoning that expands, then settles\n * Search web-search trace: query + sources read\n * Coding tool trace: files read, edits, commands\n *\n * The trace runs once, settles, and remains expandable.\n * ───────────────────────────────────────────────────────── */\n\nconst STAGES = [800, 600, 1800, 2600, 1600];\n\nfunction useSequence(steps: number[]) {\n const [stage, setStage] = useState(0);\n useEffect(() => {\n if (stage >= steps.length - 1) return;\n const t = setTimeout(() => setStage((s) => s + 1), steps[stage]);\n return () => clearTimeout(t);\n }, [stage, steps]);\n return stage;\n}\n\ntype Row = {\n primary: string;\n secondary?: string;\n mono?: boolean;\n add?: number;\n del?: number;\n href?: string;\n};\n\nconst VARIANTS: Record<\n string,\n { active: string; done: string; rows: Row[]; query?: string }\n> = {\n Steps: {\n active: \"Thinking\",\n done: \"Thought for 4 seconds\",\n rows: [\n { primary: \"Reading flavor briefs\" },\n { primary: \"Scanning supplier lists\" },\n { primary: \"Comparing tasting notes\", secondary: \"6 flavors\" },\n { primary: \"Writing the scoop report\" },\n ],\n },\n Reasoning: {\n active: \"Thinking\",\n done: \"Thought for 4 seconds\",\n rows: [\n { primary: \"Summer demand spikes for stone-fruit flavors — peach and apricot lead.\" },\n { primary: \"I should check cone inventory before promoting a waffle-bowl special.\" },\n ],\n },\n Search: {\n active: \"Searching the web\",\n done: \"Searched the web\",\n query: \"best waffle cone supplier\",\n rows: [\n { primary: \"Joy Cone\", secondary: \"joycone.com\", href: \"https://joycone.com/fs_products/waffle-cones/\" },\n { primary: \"WebstaurantStore\", secondary: \"webstaurantstore.com\", href: \"https://www.webstaurantstore.com/ice-cream-shop-supplies.html\" },\n { primary: \"The Konery\", secondary: \"thekonery.com\", href: \"https://www.thekonery.com/\" },\n ],\n },\n Coding: {\n active: \"Running tools\",\n done: \"Ran 3 tools\",\n rows: [\n { primary: \"Read\", secondary: \"flavors.ts\", mono: true },\n { primary: \"Edit\", secondary: \"ChurnSchedule.tsx\", mono: true, add: 74, del: 41 },\n { primary: \"Run\", secondary: \"npm run freeze\", mono: true },\n ],\n },\n};\n\nfunction Dot({ tone }: { tone: string }) {\n return (\n \n \n \n \n \n \n );\n}\n\nconst TONES = [\"bg-accent\", \"bg-orange\", \"bg-green\"];\n\nexport default function ThinkingState({ variant = \"Steps\", onSettled }: { variant?: string; onSettled?: () => void }) {\n const stage = useSequence(STAGES);\n const [manualExpanded, setManualExpanded] = useState(null);\n const [selectedTool, setSelectedTool] = useState(null);\n const v = VARIANTS[variant] ?? VARIANTS.Steps;\n const autoExpanded = stage >= 1 && stage < 4;\n const expanded = manualExpanded ?? autoExpanded;\n const working = stage < 3;\n const visible = stage < 2 ? 0 : stage === 2 ? Math.min(2, v.rows.length) : v.rows.length;\n const traceRef = useRef(null);\n const [lineHeight, setLineHeight] = useState(0);\n useLayoutEffect(() => {\n if (traceRef.current) setLineHeight(traceRef.current.offsetHeight);\n }, [visible, expanded, variant, stage]);\n\n /* let embedders sequence content after the trace settles */\n const settledRef = useRef(false);\n useEffect(() => {\n if (working || settledRef.current) return;\n settledRef.current = true;\n onSettled?.();\n }, [working, onSettled]);\n\n return (\n \n {/* header — shared across variants */}\n setManualExpanded((current) => !(current ?? autoExpanded))}\n className=\"-mx-1.5 flex w-fit items-center gap-2 rounded-control px-1.5 py-1\n transition-colors duration-100 hover:bg-hover-2\"\n >\n \n \n \n \n {working ? (\n \n {v.active}\n \n ) : (\n \n {v.done}\n \n )}\n \n \n \n \n \n\n {/* expandable trace */}\n \n
\n
\n \n
\n {v.query && (\n
\n \n \n \n \n {v.query}\n
\n )}\n {v.rows.slice(0, visible).map((row, i) => {\n const content = (\n <>\n {variant === \"Search\" && }\n {variant === \"Steps\" && (\n i < visible - 1 || !working ? (\n \n \n \n ) : (\n \n )\n )}\n \n {row.primary}\n \n {row.secondary && (\n \n {row.secondary}\n \n )}\n {row.add !== undefined && (\n \n +{row.add}{\" \"}\n −{row.del}\n \n )}\n \n );\n const rowClass = \"flex min-h-7 w-full items-center gap-2 rounded-[6px] px-1.5 py-0.5 text-left\";\n const animation = { animation: `fade-up 320ms cubic-bezier(0.23,1,0.32,1) ${i * 120}ms both` };\n\n if (variant === \"Search\") {\n return (\n \n {content}\n \n );\n }\n\n if (variant === \"Coding\") {\n const selected = selectedTool === row.primary;\n return (\n setSelectedTool(selected ? null : row.primary)}\n className={`${rowClass} transition-colors duration-150 ${selected ? \"bg-inset\" : \"hover:bg-hover\"}`}\n style={animation}\n >\n {content}\n \n );\n }\n\n return (\n
\n {content}\n
\n );\n })}\n {variant === \"Search\" && stage >= 3 && (\n \n +7 more\n \n )}\n
\n
\n
\n \n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/ThinkingState.tsx" - } - ] -} diff --git a/scripts/beautifului/tool-chips.json b/scripts/beautifului/tool-chips.json deleted file mode 100644 index 79d8caf..0000000 --- a/scripts/beautifului/tool-chips.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "tool-chips", - "type": "registry:component", - "title": "Tool Chips", - "description": "Code edits and tool calls as compact chips.", - "registryDependencies": [ - "https://www.beautifului.dev/r/foundation.json" - ], - "files": [ - { - "path": "components/primitives/ToolChips.tsx", - "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\n/* ─────────────────────────────────────────────────────────\n * TOOL CHIPS\n * An agent run as compact rows: tool calls with inline\n * chips, then file-diff chips summarizing the edits.\n * Hover a row to reveal its chevron; every row expands\n * to show what the tool actually did.\n * ───────────────────────────────────────────────────────── */\n\nconst STEP_MS = 700;\n\nconst Icons: Record = {\n think: ,\n write: ,\n run: ,\n read: ,\n};\n\nexport type ToolDetailLine = { text: string; tone?: \"add\" };\n\nexport type ToolStep = {\n icon: string;\n label: string;\n chip: string;\n mono: boolean;\n detailMono: boolean;\n detail: ToolDetailLine[];\n};\n\nexport type ToolDiff = { file: string; add: number; del: number };\n\nexport type ToolDiffLine = { text: string; tone: \"add\" | \"del\" | \"ctx\" };\n\nexport type ToolChipsLabels = {\n header: string;\n more: string;\n};\n\nconst DEFAULT_LABELS: ToolChipsLabels = {\n header: \"4 tool calls, 2 messages\",\n more: \"+2 more\",\n};\n\nconst ROWS: ToolStep[] = [\n {\n icon: \"think\", label: \"Thinking\", chip: \"Planning the churn schedule…\", mono: false, detailMono: false,\n detail: [\n { text: \"Weekend demand carries pistachio, so it churns first.\" },\n { text: \"Batch capacity leaves two evening freezer windows.\" },\n ],\n },\n {\n icon: \"write\", label: \"Write 204 lines\", chip: \"ChurnSchedule.tsx\", mono: true, detailMono: true,\n detail: [\n { text: \"+ const windows = slots.filter((s) => s.temp <= -12)\", tone: \"add\" },\n { text: \"+ return schedule(windows, { hero: \\\"pistachio\\\" })\", tone: \"add\" },\n ],\n },\n {\n icon: \"run\", label: \"Rebuild and verify\", chip: \"npm run freeze\", mono: true, detailMono: true,\n detail: [\n { text: \"✓ built in 1.2s\" },\n { text: \"✓ 34 checks passed\" },\n ],\n },\n {\n icon: \"read\", label: \"Read image\", chip: \"flavor-chart.png\", mono: true, detailMono: false,\n detail: [\n { text: \"1280 × 720 · line chart, three summers.\" },\n { text: \"Mint chip trends up 12% through July.\" },\n ],\n },\n];\n\nconst DIFFS: ToolDiff[] = [\n { file: \"flavors.css\", add: 13, del: 0 },\n { file: \"ChurnSchedule.tsx\", add: 74, del: 41 },\n { file: \"menu.ts\", add: 8, del: 2 },\n];\n\n/* hovering a file chip opens its diff — green added, red removed */\nconst DIFF_LINES: Record = {\n \"flavors.css\": [\n { text: \".scoop-card {\", tone: \"ctx\" },\n { text: \" gap: 14px;\", tone: \"del\" },\n { text: \" gap: 12px;\", tone: \"add\" },\n { text: \" container-type: inline-size;\", tone: \"add\" },\n { text: \"}\", tone: \"ctx\" },\n ],\n \"ChurnSchedule.tsx\": [\n { text: \"const slots = coldSlots(week);\", tone: \"ctx\" },\n { text: \"const windows = slots;\", tone: \"del\" },\n { text: \"const windows = slots.filter(\", tone: \"add\" },\n { text: \" (s) => s.temp <= -12,\", tone: \"add\" },\n { text: \");\", tone: \"add\" },\n ],\n \"menu.ts\": [\n { text: \"export const hero = \\\"mint-chip\\\";\", tone: \"del\" },\n { text: \"export const hero = \\\"pistachio\\\";\", tone: \"add\" },\n ],\n};\n\nexport default function ToolChips({\n steps = ROWS,\n diffs = DIFFS,\n diffLines = DIFF_LINES,\n labels,\n className,\n onOpenChange,\n onToggleRow,\n}: {\n /** Accepted for gallery/registry parity; ToolChips has no visual variants. */\n variant?: string;\n steps?: ToolStep[];\n diffs?: ToolDiff[];\n diffLines?: Record;\n labels?: Partial;\n className?: string;\n onOpenChange?: (open: boolean) => void;\n onToggleRow?: (label: string, open: boolean) => void;\n} = {}) {\n const copy = { ...DEFAULT_LABELS, ...labels };\n const [step, setStep] = useState(0);\n const [open, setOpen] = useState(true);\n const [openRows, setOpenRows] = useState>(new Set());\n /* Rendered in a body portal so animated/translated reply wrappers cannot\n * redefine the fixed-position coordinate system. */\n const [preview, setPreview] = useState<{\n file: string;\n x: number;\n top?: number;\n bottom?: number;\n } | null>(null);\n const openPreview = (file: string) => (event: React.SyntheticEvent) => {\n const rect = (event.currentTarget as Element).closest(\"[data-diffchip]\")!.getBoundingClientRect();\n const previewHeight = 38 + (diffLines[file]?.length ?? 0) * 19;\n const fitsBelow = rect.bottom + 6 + previewHeight <= window.innerHeight - 12;\n setPreview({\n file,\n x: Math.max(12, Math.min(rect.left, window.innerWidth - 300)),\n ...(fitsBelow\n ? { top: rect.bottom + 6 }\n : { bottom: window.innerHeight - rect.top + 6 }),\n });\n };\n const closePreview = (file: string) => () =>\n setPreview((current) => (current?.file === file ? null : current));\n const total = steps.length + 1; // rows, then diff chips\n\n useEffect(() => {\n if (step >= total) return;\n const t = setTimeout(() => setStep((s) => s + 1), STEP_MS);\n return () => clearTimeout(t);\n }, [step, total]);\n\n const toggleRow = (label: string) =>\n setOpenRows((current) => {\n const next = new Set(current);\n next.has(label) ? next.delete(label) : next.add(label);\n onToggleRow?.(label, next.has(label));\n return next;\n });\n\n return (\n
\n {/* collapsed run header */}\n \n setOpen((current) => {\n onOpenChange?.(!current);\n return !current;\n })\n }\n className=\"-mx-1.5 flex w-fit items-center gap-1.5 rounded-control px-1.5 py-1 text-[12.5px] text-ink-2 transition-colors duration-100 hover:bg-hover-2\"\n >\n \n \n \n {copy.header}\n \n\n {/* tool call rows */}\n
\n {/* -mx-1 + px-1.5 keeps content at the same x while giving the\n row hover pills room inside this overflow-hidden clip box */}\n
\n
\n {steps.slice(0, step).map((row) => {\n const rowOpen = openRows.has(row.label);\n return (\n
\n toggleRow(row.label)}\n className=\"group/row -mx-[3px] flex h-7 w-[calc(100%+6px)] min-w-0 items-center gap-2 rounded-control px-[3px] text-left transition-colors duration-100 hover:bg-hover-2\"\n >\n \n \n {Icons[row.icon]}\n \n \n \n \n \n {row.label}\n \n {row.chip}\n \n \n\n {/* expanded detail */}\n \n
\n
\n {row.detail.map((line) => (\n \n {line.text}\n \n ))}\n
\n
\n
\n
\n );\n })}\n
\n\n {/* file-diff chips */}\n {step >= total && (\n
\n {diffs.map((d, i) => (\n \n \n {d.file}\n +{d.add}\n {d.del > 0 && −{d.del}}\n \n\n \n ))}\n \n {copy.more}\n \n
\n )}\n
\n
\n {preview && typeof document !== \"undefined\" && createPortal(\n \n
\n {preview.file}\n \n +{diffs.find((diff) => diff.file === preview.file)?.add}\n {(diffs.find((diff) => diff.file === preview.file)?.del ?? 0) > 0 && (\n −{diffs.find((diff) => diff.file === preview.file)?.del}\n )}\n \n
\n
\n {(diffLines[preview.file] ?? []).map((line, index) => (\n \n {line.tone === \"add\" ? \"+\" : line.tone === \"del\" ? \"−\" : \" \"}\n {line.text}\n
\n ))}\n \n ,\n document.body,\n )}\n \n );\n}\n", - "type": "registry:component", - "target": "components/primitives/ToolChips.tsx" - } - ] -} diff --git a/scripts/check-lu-icons.mjs b/scripts/check-lu-icons.mjs deleted file mode 100644 index 7086ac9..0000000 --- a/scripts/check-lu-icons.mjs +++ /dev/null @@ -1,18 +0,0 @@ -// 校验 react-icons/lu 里要用的图标名是否存在 -import { readdirSync, readFileSync } from 'node:fs' - -const pnpm = new URL('../node_modules/.pnpm/', import.meta.url) -const dir = readdirSync(pnpm).find((d) => d.startsWith('react-icons@')) -if (!dir) throw new Error('react-icons not found') -const dts = new URL(`${dir}/node_modules/react-icons/lu/index.d.ts`, pnpm) -const content = readFileSync(dts, 'utf8') - -const names = [ - 'LuCheck', 'LuCpu', 'LuMessageSquare', 'LuPencil', 'LuRefreshCw', 'LuSettings', 'LuStore', - 'LuTrash2', 'LuX', 'LuAlertTriangle', 'LuTriangleAlert', 'LuRotateCw', 'LuShieldAlert', - 'LuTimer', 'LuKeyRound', 'LuLogOut', 'LuSearch', 'LuSparkles', 'LuPlus', 'LuArrowUp', - 'LuSquare', 'LuChevronDown', 'LuActivity' -] -for (const n of names) { - console.log(n, content.includes(`declare const ${n}:`) ? 'OK' : 'MISSING') -} diff --git a/scripts/extract-beautifului.mjs b/scripts/extract-beautifului.mjs deleted file mode 100644 index d2e1dad..0000000 --- a/scripts/extract-beautifului.mjs +++ /dev/null @@ -1,52 +0,0 @@ -// 补抓 thinking-state + 解包全部 JSON 到暂存目录,并打印每个组件的 import / 导出签名 -import { mkdir, writeFile, readFile } from 'node:fs/promises' - -const jsonDir = new URL('./beautifului/', import.meta.url) -const stageDir = new URL('./beautifului/stage/', import.meta.url) -await mkdir(stageDir, { recursive: true }) - -// 1) 补抓 thinking-state -const tsRes = await fetch('https://www.beautifului.dev/r/thinking-state.json') -if (tsRes.ok) await writeFile(new URL('thinking-state.json', jsonDir), await tsRes.text()) -else console.log('thinking-state => HTTP', tsRes.status) - -// 2) 解包 + 签名摘要 -const slugs = [ - 'foundation', - 'loading-state', - 'thinking-state', - 'streaming-text', - 'tool-chips', - 'approval-card', - 'button', - 'glide-menu' -] - -for (const s of slugs) { - let raw - try { - raw = await readFile(new URL(`${s}.json`, jsonDir), 'utf8') - } catch { - console.log(`### ${s}: JSON 缺失`) - continue - } - const j = JSON.parse(raw) - console.log(`\n### ${s} (${j.type}) deps=${JSON.stringify(j.dependencies || [])}`) - if (j.css) { - await writeFile(new URL(`${s}.css`, stageDir), j.css) - console.log(` css -> stage/${s}.css (${j.css.length} chars)`) - } - for (const f of j.files || []) { - const name = f.path.split('/').pop() - await writeFile(new URL(name, stageDir), f.content ?? '') - console.log(` ${f.path} -> stage/${name} (${(f.content ?? '').length} chars)`) - if (f.content) { - const imports = f.content - .split('\n') - .filter((l) => /^(import|export)\b/.test(l.trim()) && /from ['"]/.test(l)) - for (const l of imports) console.log(' ', l.trim()) - const exports = f.content.match(/^export (function|const|interface|type|default)[^\n({]*/gm) || [] - for (const e of exports) console.log(' EXP:', e.trim()) - } - } -} diff --git a/scripts/fetch-beautifului.mjs b/scripts/fetch-beautifului.mjs deleted file mode 100644 index bf647a0..0000000 --- a/scripts/fetch-beautifului.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// 下载 beautifului.dev registry:索引 + 候选组件 JSON,保存到 scripts/beautifului/ -import { mkdir, writeFile } from 'node:fs/promises' - -const outDir = new URL('./beautifului/', import.meta.url) -await mkdir(outDir, { recursive: true }) - -async function grab(slug) { - const r = await fetch(`https://www.beautifului.dev/r/${slug}.json`) - if (!r.ok) { - console.log(slug, '=> HTTP', r.status) - return null - } - const text = await r.text() - await writeFile(new URL(`${slug}.json`, outDir), text) - return JSON.parse(text) -} - -// 1) registry 索引 -const idx = await grab('registry') -if (idx) { - const items = Array.isArray(idx) ? idx : idx.items || [] - console.log('=== registry index ===') - for (const it of items) console.log('-', it.name, '|', it.title || '', '|', it.type || '') -} - -// 2) 候选组件(含传递依赖) -const wanted = [ - 'foundation', - 'loading-state', - 'streaming-text', - 'tool-chips', - 'approval-card', - 'button', - 'glide-menu', - 'task-rows', - 'prompt-bar', - 'selection-actions', - 'shimmer', - 'stream-text' -] -console.log('=== components ===') -for (const s of wanted) { - const j = await grab(s) - if (!j) continue - const files = (j.files || []).map((f) => f.path).join(', ') - console.log(`${s}: deps=${JSON.stringify(j.dependencies || [])} files=[${files}]`) -} diff --git a/scripts/install-beautifului.mjs b/scripts/install-beautifului.mjs deleted file mode 100644 index abf7ba8..0000000 --- a/scripts/install-beautifului.mjs +++ /dev/null @@ -1,20 +0,0 @@ -// 把 registry 原版组件复制进 renderer(保持 beautifului.dev 原始源码) -import { mkdir, copyFile } from 'node:fs/promises' - -const stage = new URL('./beautifului/stage/', import.meta.url) -const root = new URL('../apps/desktop/src/renderer/src/components/', import.meta.url) - -await mkdir(new URL('atoms/', root), { recursive: true }) -await mkdir(new URL('primitives/', root), { recursive: true }) - -const verbatim = [ - ['Button.tsx', 'atoms/Button.tsx'], - ['GlideMenu.tsx', 'primitives/GlideMenu.tsx'], - ['LoadingState.tsx', 'primitives/LoadingState.tsx'], - ['ApprovalCard.tsx', 'primitives/ApprovalCard.tsx'] -] - -for (const [from, to] of verbatim) { - await copyFile(new URL(from, stage), new URL(to, root)) - console.log('copied', from, '->', to) -} diff --git a/scripts/probe-pi-sdk.mjs b/scripts/probe-pi-sdk.mjs deleted file mode 100644 index e2fc207..0000000 --- a/scripts/probe-pi-sdk.mjs +++ /dev/null @@ -1,32 +0,0 @@ -// 检查 pi-coding-agent 的 SessionManager / AgentSession 类型定义,确认会话落盘时机 -import { readdirSync, readFileSync, existsSync } from 'node:fs' - -const pnpm = new URL('../node_modules/.pnpm/', import.meta.url) -const dir = readdirSync(pnpm).find((d) => d.startsWith('@earendil-works+pi-coding-agent@')) -if (!dir) throw new Error('pi-coding-agent not found') -const dist = new URL(`${dir}/node_modules/@earendil-works/pi-coding-agent/dist/`, pnpm) -console.log('dist exists:', existsSync(dist)) - -// 在 dist 里 grep 关键定义 -const { execSync } = await import('node:child_process') -const distPath = new URL('.', dist).pathname.replace(/^\/([A-Za-z]:)/, '$1') -const grep = (pattern) => { - try { - const out = execSync( - `findstr /s /n /c:"${pattern}" "${distPath}*.d.ts"`, - { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 } - ) - console.log(`\n=== ${pattern} ===`) - console.log(out.split('\n').slice(0, 40).join('\n')) - } catch (e) { - console.log(`\n=== ${pattern} === (no match)`) - } -} - -grep('static list(') -grep('sessionFile') -grep('get sessionName') -grep('setSessionName') -grep('static continueRecent') -grep('class SessionManager') -grep('interface SessionInfo')