@braedonsaunders/appkit-ai 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +30 -0
- package/agent.d.ts +33 -0
- package/agent.d.ts.map +1 -0
- package/agent.js +53 -0
- package/agent.js.map +1 -0
- package/analysis.d.ts +72 -0
- package/analysis.d.ts.map +1 -0
- package/analysis.js +114 -0
- package/analysis.js.map +1 -0
- package/builder.d.ts +7 -0
- package/builder.d.ts.map +1 -0
- package/builder.js +28 -0
- package/builder.js.map +1 -0
- package/client.d.ts +91 -0
- package/client.d.ts.map +1 -0
- package/client.js +314 -0
- package/client.js.map +1 -0
- package/context.d.ts +18 -0
- package/context.d.ts.map +1 -0
- package/context.js +84 -0
- package/context.js.map +1 -0
- package/digest.d.ts +13 -0
- package/digest.d.ts.map +1 -0
- package/digest.js +23 -0
- package/digest.js.map +1 -0
- package/doc-chat.d.ts +36 -0
- package/doc-chat.d.ts.map +1 -0
- package/doc-chat.js +106 -0
- package/doc-chat.js.map +1 -0
- package/extract.d.ts +10 -0
- package/extract.d.ts.map +1 -0
- package/extract.js +31 -0
- package/extract.js.map +1 -0
- package/index.d.ts +14 -0
- package/index.d.ts.map +1 -0
- package/index.js +14 -0
- package/index.js.map +1 -0
- package/models.d.ts +11 -0
- package/models.d.ts.map +1 -0
- package/models.js +104 -0
- package/models.js.map +1 -0
- package/package.json +77 -0
- package/prompts.d.ts +11 -0
- package/prompts.d.ts.map +1 -0
- package/prompts.js +22 -0
- package/prompts.js.map +1 -0
- package/react.d.ts +47 -0
- package/react.d.ts.map +1 -0
- package/react.js +137 -0
- package/react.js.map +1 -0
- package/vision.d.ts +66 -0
- package/vision.d.ts.map +1 -0
- package/vision.js +137 -0
- package/vision.js.map +1 -0
- package/writing.d.ts +15 -0
- package/writing.d.ts.map +1 -0
- package/writing.js +46 -0
- package/writing.js.map +1 -0
package/react.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { parseJsonEventStream, readUIMessageStream, uiMessageChunkSchema, } from 'ai';
|
|
5
|
+
import { AlertCircle, CheckCircle2, ChevronRight, Database, Loader2, Send, Sparkles, Square, } from 'lucide-react';
|
|
6
|
+
import Markdown from 'react-markdown';
|
|
7
|
+
import remarkGfm from 'remark-gfm';
|
|
8
|
+
import { Button, EmptyState, UiLink, cn } from '@braedonsaunders/appkit-ui';
|
|
9
|
+
const DEFAULT_LABELS = {
|
|
10
|
+
title: 'Assistant',
|
|
11
|
+
welcomeTitle: 'How can I help?',
|
|
12
|
+
welcomeDescription: 'Ask about your workspace or let the assistant use an approved tool.',
|
|
13
|
+
disabledTitle: 'Connect an AI provider',
|
|
14
|
+
disabledDescription: 'Connect an AI provider to enable agent conversations. No provider credentials are included in the demo.',
|
|
15
|
+
placeholder: 'Ask the assistant…',
|
|
16
|
+
send: 'Send',
|
|
17
|
+
stop: 'Stop generating',
|
|
18
|
+
failed: 'The assistant could not complete that turn. Please try again.',
|
|
19
|
+
input: 'Input',
|
|
20
|
+
result: 'Result',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The streaming thread/composer extracted from the sibling assistant. The app
|
|
24
|
+
* owns persistence and the HTTP transport; appkit owns UI-message decoding,
|
|
25
|
+
* cancellation, ordered part rendering, and tool cards.
|
|
26
|
+
*/
|
|
27
|
+
export function AgentPanel({ enabled, initialMessages = [], suggestions = [], labels: labelOverrides, send, maxPromptCharacters = 32_000, toolLabels, }) {
|
|
28
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
29
|
+
const [messages, setMessages] = React.useState(initialMessages);
|
|
30
|
+
const [input, setInput] = React.useState('');
|
|
31
|
+
const [streaming, setStreaming] = React.useState(false);
|
|
32
|
+
const [error, setError] = React.useState(null);
|
|
33
|
+
const abortRef = React.useRef(null);
|
|
34
|
+
const bottomRef = React.useRef(null);
|
|
35
|
+
const scrollToBottom = React.useCallback(() => {
|
|
36
|
+
window.requestAnimationFrame(() => bottomRef.current?.scrollIntoView({ block: 'end' }));
|
|
37
|
+
}, []);
|
|
38
|
+
const submit = React.useCallback(async (raw) => {
|
|
39
|
+
const prompt = raw.trim();
|
|
40
|
+
if (!enabled || !send || !prompt || prompt.length > maxPromptCharacters || abortRef.current)
|
|
41
|
+
return;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
abortRef.current = controller;
|
|
44
|
+
const stamp = Date.now();
|
|
45
|
+
setInput('');
|
|
46
|
+
setError(null);
|
|
47
|
+
setMessages((current) => [...current, { id: `user-${stamp}`, role: 'user', parts: [{ type: 'text', text: prompt }] }, { id: `assistant-${stamp}`, role: 'assistant', parts: [] }]);
|
|
48
|
+
setStreaming(true);
|
|
49
|
+
scrollToBottom();
|
|
50
|
+
let producedParts = false;
|
|
51
|
+
try {
|
|
52
|
+
const response = await send(prompt, controller.signal);
|
|
53
|
+
if (!response.ok || !response.body)
|
|
54
|
+
throw new Error('agent request failed');
|
|
55
|
+
const chunks = parseJsonEventStream({ stream: response.body, schema: uiMessageChunkSchema }).pipeThrough(new TransformStream({ transform(part, stream) { if (part.success && part.value)
|
|
56
|
+
stream.enqueue(part.value); } }));
|
|
57
|
+
let lastParts = [];
|
|
58
|
+
for await (const message of readUIMessageStream({ stream: chunks })) {
|
|
59
|
+
lastParts = message.parts;
|
|
60
|
+
producedParts = lastParts.length > 0;
|
|
61
|
+
setMessages((current) => replaceLastAssistantParts(current, lastParts));
|
|
62
|
+
scrollToBottom();
|
|
63
|
+
}
|
|
64
|
+
if (lastParts.length === 0 && !controller.signal.aborted)
|
|
65
|
+
setError(labels.failed);
|
|
66
|
+
}
|
|
67
|
+
catch (reason) {
|
|
68
|
+
if (reason.name !== 'AbortError')
|
|
69
|
+
setError(labels.failed);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (!producedParts) {
|
|
73
|
+
setMessages((current) => current.filter((message) => message.id !== `assistant-${stamp}`));
|
|
74
|
+
}
|
|
75
|
+
setStreaming(false);
|
|
76
|
+
if (abortRef.current === controller)
|
|
77
|
+
abortRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
}, [enabled, labels.failed, maxPromptCharacters, scrollToBottom, send]);
|
|
80
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col bg-bg-subtle", children: [_jsxs("header", { className: "flex h-12 shrink-0 items-center gap-2 border-b border-border bg-surface px-4", children: [_jsx(Sparkles, { size: 16, className: "text-primary" }), _jsx("span", { className: "text-sm font-medium text-fg", children: labels.title })] }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto", children: _jsxs("div", { className: "mx-auto w-full max-w-3xl px-4 py-6", children: [messages.length === 0 ? _jsx(AgentWelcome, { enabled: enabled, title: enabled ? labels.welcomeTitle : labels.disabledTitle, description: enabled ? labels.welcomeDescription : labels.disabledDescription, suggestions: suggestions, onPick: (value) => void submit(value) }) : _jsx("div", { className: "space-y-6", children: messages.map((message) => message.role === 'system' ? null : _jsx(AgentMessageRow, { message: message, streaming: streaming, labels: labels, toolLabels: toolLabels }, message.id)) }), error ? _jsx("div", { role: "alert", className: "mt-5 rounded-lg border border-danger/25 bg-danger-subtle px-3 py-2 text-sm text-danger", children: error }) : null, _jsx("div", { ref: bottomRef })] }) }), enabled ? _jsx("div", { className: "shrink-0 border-t border-border bg-surface px-4 py-3", children: _jsx("div", { className: "mx-auto w-full max-w-3xl", children: _jsxs("div", { className: "flex items-end gap-2 rounded-2xl border border-border-strong bg-surface p-2 shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-ring/20", children: [_jsx("textarea", { value: input, onChange: (event) => setInput(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter' && !event.shiftKey) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
void submit(input);
|
|
83
|
+
} }, maxLength: maxPromptCharacters, rows: 1, placeholder: labels.placeholder, className: "max-h-40 flex-1 resize-none overflow-y-auto border-0 bg-transparent px-2 py-1.5 text-base text-fg outline-none placeholder:text-fg-subtle sm:text-sm" }), streaming ? _jsx(Button, { type: "button", variant: "outline", size: "icon", onClick: () => abortRef.current?.abort(), "aria-label": labels.stop, children: _jsx(Square, { size: 16 }) }) : _jsx(Button, { type: "button", size: "icon", onClick: () => void submit(input), disabled: !input.trim() || !send, "aria-label": labels.send, children: _jsx(Send, { size: 16 }) })] }) }) }) : null] }));
|
|
84
|
+
}
|
|
85
|
+
function replaceLastAssistantParts(messages, parts) {
|
|
86
|
+
const copy = messages.slice();
|
|
87
|
+
for (let index = copy.length - 1; index >= 0; index -= 1) {
|
|
88
|
+
if (copy[index]?.role === 'assistant') {
|
|
89
|
+
copy[index] = { ...copy[index], parts };
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return copy;
|
|
94
|
+
}
|
|
95
|
+
function AgentWelcome({ enabled, title, description, suggestions, onPick }) {
|
|
96
|
+
return _jsxs("div", { className: "pt-10", children: [_jsx(EmptyState, { icon: _jsx(Sparkles, {}), title: title, description: description }), enabled && suggestions.length ? _jsx("div", { className: "mx-auto mt-6 grid max-w-2xl gap-2 sm:grid-cols-2", children: suggestions.map((suggestion) => _jsx("button", { type: "button", onClick: () => onPick(suggestion), className: "rounded-xl border border-border bg-surface px-4 py-3 text-left text-sm text-fg-muted shadow-sm transition-colors hover:border-primary/40 hover:bg-primary-subtle hover:text-fg", children: suggestion }, suggestion)) }) : null] });
|
|
97
|
+
}
|
|
98
|
+
function AgentMessageRow({ message, streaming, labels, toolLabels }) {
|
|
99
|
+
if (message.role === 'user') {
|
|
100
|
+
const text = message.parts.find((part) => part.type === 'text')?.text;
|
|
101
|
+
return _jsx("div", { className: "flex justify-end", children: _jsx("div", { className: "max-w-[85%] rounded-2xl rounded-br-md bg-primary px-4 py-2 text-sm whitespace-pre-wrap text-primary-fg", children: text }) });
|
|
102
|
+
}
|
|
103
|
+
return _jsxs("div", { className: "flex gap-3", children: [_jsx("span", { className: "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-fg shadow-sm", children: _jsx(Sparkles, { size: 16 }) }), _jsx("div", { className: "min-w-0 flex-1 pt-0.5", children: message.parts.length === 0 && streaming ? _jsx("div", { className: "flex items-center gap-1 py-1.5", children: [0, 1, 2].map((index) => _jsx("span", { className: "size-1.5 animate-bounce rounded-full bg-fg-subtle", style: { animationDelay: `${index * 0.15}s` } }, index)) }) : _jsx(AgentMessageParts, { parts: message.parts, labels: labels, toolLabels: toolLabels }) })] });
|
|
104
|
+
}
|
|
105
|
+
function AgentMessageParts({ parts, labels, toolLabels }) {
|
|
106
|
+
return _jsx("div", { className: "space-y-2.5", children: parts.map((part, index) => {
|
|
107
|
+
if (part.type === 'text')
|
|
108
|
+
return typeof part.text === 'string' && part.text.trim() ? _jsx(ChatMarkdown, { children: part.text }, index) : null;
|
|
109
|
+
if (part.type === 'step-start' || part.type === 'reasoning')
|
|
110
|
+
return null;
|
|
111
|
+
if (part.type === 'dynamic-tool' || part.type.startsWith('tool-')) {
|
|
112
|
+
const name = part.type === 'dynamic-tool' ? String(part.toolName ?? 'tool') : part.type.slice(5);
|
|
113
|
+
return _jsx(AgentToolCard, { name: name, label: toolLabels?.[name], state: String(part.state ?? 'output-available'), input: part.input, output: part.output, inputLabel: labels.input, resultLabel: labels.result }, index);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}) });
|
|
117
|
+
}
|
|
118
|
+
export function ChatMarkdown({ children }) {
|
|
119
|
+
return _jsx("div", { className: "space-y-2 text-sm leading-relaxed text-fg", children: _jsx(Markdown, { remarkPlugins: [remarkGfm], components: { p: ({ children: content }) => _jsx("p", { className: "whitespace-pre-wrap", children: content }), h1: ({ children: content }) => _jsx("h1", { className: "text-lg font-semibold", children: content }), h2: ({ children: content }) => _jsx("h2", { className: "text-base font-semibold", children: content }), ul: ({ children: content }) => _jsx("ul", { className: "list-disc space-y-1 pl-5", children: content }), ol: ({ children: content }) => _jsx("ol", { className: "list-decimal space-y-1 pl-5", children: content }), code: ({ children: content }) => _jsx("code", { className: "rounded bg-bg-subtle px-1 py-0.5 font-mono text-[0.85em] text-primary", children: content }), pre: ({ children: content }) => _jsx("pre", { className: "overflow-auto rounded-lg bg-overlay p-3 text-sm whitespace-pre-wrap text-white", children: content }), table: ({ children: content }) => _jsx("div", { className: "overflow-x-auto", children: _jsx("table", { className: "w-full border-collapse text-sm", children: content }) }), th: ({ children: content }) => _jsx("th", { className: "border-b border-border px-2 py-1 text-left", children: content }), td: ({ children: content }) => _jsx("td", { className: "border-b border-border-subtle px-2 py-1", children: content }), a: ({ href, children: content }) => href?.startsWith('/') ? _jsx(UiLink, { href: href, className: "font-medium text-primary underline-offset-2 hover:underline", children: content }) : _jsx("a", { href: href, target: "_blank", rel: "noreferrer", className: "font-medium text-primary underline-offset-2 hover:underline", children: content }) }, children: children }) });
|
|
120
|
+
}
|
|
121
|
+
export function AgentToolCard({ name, label, state, input, output, inputLabel = 'Input', resultLabel = 'Result' }) {
|
|
122
|
+
const [open, setOpen] = React.useState(false);
|
|
123
|
+
const running = state === 'input-streaming' || state === 'input-available';
|
|
124
|
+
const errored = state === 'output-error' || output?.ok === false;
|
|
125
|
+
return _jsxs("div", { className: "overflow-hidden rounded-lg border border-border bg-bg-subtle text-sm", children: [_jsxs("button", { type: "button", onClick: () => setOpen((value) => !value), className: "flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-surface-hover", children: [_jsx("span", { className: cn('flex size-6 shrink-0 items-center justify-center rounded-md', errored ? 'bg-danger-subtle text-danger' : 'bg-primary-subtle text-primary'), children: _jsx(Database, { size: 14 }) }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-fg", children: label ?? name.replaceAll('_', ' ') }), running ? _jsx(Loader2, { size: 14, className: "animate-spin text-fg-subtle" }) : errored ? _jsx(AlertCircle, { size: 14, className: "text-danger" }) : _jsx(CheckCircle2, { size: 14, className: "text-success" }), _jsx(ChevronRight, { size: 14, className: cn('text-fg-subtle transition-transform', open && 'rotate-90') })] }), open ? _jsxs("div", { className: "space-y-2 border-t border-border px-3 py-2", children: [input !== undefined ? _jsx(AgentToolDetail, { label: inputLabel, value: input }) : null, output !== undefined ? _jsx(AgentToolDetail, { label: resultLabel, value: output }) : null] }) : null] });
|
|
126
|
+
}
|
|
127
|
+
function AgentToolDetail({ label, value }) {
|
|
128
|
+
let text;
|
|
129
|
+
try {
|
|
130
|
+
text = JSON.stringify(value, null, 2);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
text = String(value);
|
|
134
|
+
}
|
|
135
|
+
return _jsxs("div", { children: [_jsx("div", { className: "mb-1 text-[11px] font-semibold tracking-wide text-fg-subtle uppercase", children: label }), _jsx("pre", { className: "max-h-60 overflow-auto rounded-md bg-surface p-2 text-xs leading-relaxed text-fg ring-1 ring-border", children: text })] });
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=react.js.map
|
package/react.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport {\n parseJsonEventStream,\n readUIMessageStream,\n uiMessageChunkSchema,\n type UIMessageChunk,\n} from 'ai'\nimport {\n AlertCircle,\n CheckCircle2,\n ChevronRight,\n Database,\n Loader2,\n Send,\n Sparkles,\n Square,\n} from 'lucide-react'\nimport Markdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\nimport { Button, EmptyState, UiLink, cn } from '@braedonsaunders/appkit-ui'\n\nexport type AgentMessage = {\n id: string\n role: 'user' | 'assistant' | 'system'\n parts: unknown[]\n}\n\nexport type AgentPanelLabels = {\n title: string\n welcomeTitle: string\n welcomeDescription: string\n disabledTitle: string\n disabledDescription: string\n placeholder: string\n send: string\n stop: string\n failed: string\n input: string\n result: string\n}\n\nconst DEFAULT_LABELS: AgentPanelLabels = {\n title: 'Assistant',\n welcomeTitle: 'How can I help?',\n welcomeDescription: 'Ask about your workspace or let the assistant use an approved tool.',\n disabledTitle: 'Connect an AI provider',\n disabledDescription: 'Connect an AI provider to enable agent conversations. No provider credentials are included in the demo.',\n placeholder: 'Ask the assistant…',\n send: 'Send',\n stop: 'Stop generating',\n failed: 'The assistant could not complete that turn. Please try again.',\n input: 'Input',\n result: 'Result',\n}\n\nexport type AgentPanelProps = {\n enabled: boolean\n initialMessages?: AgentMessage[]\n suggestions?: string[]\n labels?: Partial<AgentPanelLabels>\n send?: (prompt: string, signal: AbortSignal) => Promise<Response>\n maxPromptCharacters?: number\n toolLabels?: Record<string, string>\n}\n\n/**\n * The streaming thread/composer extracted from the sibling assistant. The app\n * owns persistence and the HTTP transport; appkit owns UI-message decoding,\n * cancellation, ordered part rendering, and tool cards.\n */\nexport function AgentPanel({\n enabled,\n initialMessages = [],\n suggestions = [],\n labels: labelOverrides,\n send,\n maxPromptCharacters = 32_000,\n toolLabels,\n}: AgentPanelProps) {\n const labels = { ...DEFAULT_LABELS, ...labelOverrides }\n const [messages, setMessages] = React.useState(initialMessages)\n const [input, setInput] = React.useState('')\n const [streaming, setStreaming] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const abortRef = React.useRef<AbortController | null>(null)\n const bottomRef = React.useRef<HTMLDivElement>(null)\n\n const scrollToBottom = React.useCallback(() => {\n window.requestAnimationFrame(() => bottomRef.current?.scrollIntoView({ block: 'end' }))\n }, [])\n\n const submit = React.useCallback(async (raw: string) => {\n const prompt = raw.trim()\n if (!enabled || !send || !prompt || prompt.length > maxPromptCharacters || abortRef.current) return\n const controller = new AbortController()\n abortRef.current = controller\n const stamp = Date.now()\n setInput('')\n setError(null)\n setMessages((current) => [...current, { id: `user-${stamp}`, role: 'user', parts: [{ type: 'text', text: prompt }] }, { id: `assistant-${stamp}`, role: 'assistant', parts: [] }])\n setStreaming(true)\n scrollToBottom()\n let producedParts = false\n try {\n const response = await send(prompt, controller.signal)\n if (!response.ok || !response.body) throw new Error('agent request failed')\n const chunks = parseJsonEventStream({ stream: response.body, schema: uiMessageChunkSchema }).pipeThrough(new TransformStream<{ success: boolean; value?: UIMessageChunk }, UIMessageChunk>({ transform(part, stream) { if (part.success && part.value) stream.enqueue(part.value) } }))\n let lastParts: unknown[] = []\n for await (const message of readUIMessageStream({ stream: chunks })) {\n lastParts = message.parts as unknown[]\n producedParts = lastParts.length > 0\n setMessages((current) => replaceLastAssistantParts(current, lastParts))\n scrollToBottom()\n }\n if (lastParts.length === 0 && !controller.signal.aborted) setError(labels.failed)\n } catch (reason) {\n if ((reason as Error).name !== 'AbortError') setError(labels.failed)\n } finally {\n if (!producedParts) {\n setMessages((current) => current.filter((message) => message.id !== `assistant-${stamp}`))\n }\n setStreaming(false)\n if (abortRef.current === controller) abortRef.current = null\n }\n }, [enabled, labels.failed, maxPromptCharacters, scrollToBottom, send])\n\n return (\n <div className=\"flex min-h-0 flex-1 flex-col bg-bg-subtle\">\n <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border bg-surface px-4\"><Sparkles size={16} className=\"text-primary\" /><span className=\"text-sm font-medium text-fg\">{labels.title}</span></header>\n <div className=\"app-scroll min-h-0 flex-1 overflow-y-auto\"><div className=\"mx-auto w-full max-w-3xl px-4 py-6\">\n {messages.length === 0 ? <AgentWelcome enabled={enabled} title={enabled ? labels.welcomeTitle : labels.disabledTitle} description={enabled ? labels.welcomeDescription : labels.disabledDescription} suggestions={suggestions} onPick={(value) => void submit(value)} /> : <div className=\"space-y-6\">{messages.map((message) => message.role === 'system' ? null : <AgentMessageRow key={message.id} message={message} streaming={streaming} labels={labels} toolLabels={toolLabels} />)}</div>}\n {error ? <div role=\"alert\" className=\"mt-5 rounded-lg border border-danger/25 bg-danger-subtle px-3 py-2 text-sm text-danger\">{error}</div> : null}<div ref={bottomRef} />\n </div></div>\n {enabled ? <div className=\"shrink-0 border-t border-border bg-surface px-4 py-3\"><div className=\"mx-auto w-full max-w-3xl\"><div className=\"flex items-end gap-2 rounded-2xl border border-border-strong bg-surface p-2 shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-ring/20\"><textarea value={input} onChange={(event) => setInput(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); void submit(input) } }} maxLength={maxPromptCharacters} rows={1} placeholder={labels.placeholder} className=\"max-h-40 flex-1 resize-none overflow-y-auto border-0 bg-transparent px-2 py-1.5 text-base text-fg outline-none placeholder:text-fg-subtle sm:text-sm\" />{streaming ? <Button type=\"button\" variant=\"outline\" size=\"icon\" onClick={() => abortRef.current?.abort()} aria-label={labels.stop}><Square size={16} /></Button> : <Button type=\"button\" size=\"icon\" onClick={() => void submit(input)} disabled={!input.trim() || !send} aria-label={labels.send}><Send size={16} /></Button>}</div></div></div> : null}\n </div>\n )\n}\n\nfunction replaceLastAssistantParts(messages: AgentMessage[], parts: unknown[]): AgentMessage[] {\n const copy = messages.slice()\n for (let index = copy.length - 1; index >= 0; index -= 1) {\n if (copy[index]?.role === 'assistant') { copy[index] = { ...copy[index]!, parts }; break }\n }\n return copy\n}\n\nfunction AgentWelcome({ enabled, title, description, suggestions, onPick }: { enabled: boolean; title: string; description: string; suggestions: string[]; onPick: (value: string) => void }) {\n return <div className=\"pt-10\"><EmptyState icon={<Sparkles />} title={title} description={description} />{enabled && suggestions.length ? <div className=\"mx-auto mt-6 grid max-w-2xl gap-2 sm:grid-cols-2\">{suggestions.map((suggestion) => <button key={suggestion} type=\"button\" onClick={() => onPick(suggestion)} className=\"rounded-xl border border-border bg-surface px-4 py-3 text-left text-sm text-fg-muted shadow-sm transition-colors hover:border-primary/40 hover:bg-primary-subtle hover:text-fg\">{suggestion}</button>)}</div> : null}</div>\n}\n\nfunction AgentMessageRow({ message, streaming, labels, toolLabels }: { message: AgentMessage; streaming: boolean; labels: AgentPanelLabels; toolLabels?: Record<string, string> }) {\n if (message.role === 'user') {\n const text = (message.parts.find((part) => (part as { type?: string }).type === 'text') as { text?: string } | undefined)?.text\n return <div className=\"flex justify-end\"><div className=\"max-w-[85%] rounded-2xl rounded-br-md bg-primary px-4 py-2 text-sm whitespace-pre-wrap text-primary-fg\">{text}</div></div>\n }\n return <div className=\"flex gap-3\"><span className=\"mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-fg shadow-sm\"><Sparkles size={16} /></span><div className=\"min-w-0 flex-1 pt-0.5\">{message.parts.length === 0 && streaming ? <div className=\"flex items-center gap-1 py-1.5\">{[0,1,2].map((index) => <span key={index} className=\"size-1.5 animate-bounce rounded-full bg-fg-subtle\" style={{ animationDelay: `${index * 0.15}s` }} />)}</div> : <AgentMessageParts parts={message.parts} labels={labels} toolLabels={toolLabels} />}</div></div>\n}\n\nfunction AgentMessageParts({ parts, labels, toolLabels }: { parts: unknown[]; labels: AgentPanelLabels; toolLabels?: Record<string, string> }) {\n return <div className=\"space-y-2.5\">{(parts as { type: string; [key: string]: unknown }[]).map((part, index) => {\n if (part.type === 'text') return typeof part.text === 'string' && part.text.trim() ? <ChatMarkdown key={index}>{part.text}</ChatMarkdown> : null\n if (part.type === 'step-start' || part.type === 'reasoning') return null\n if (part.type === 'dynamic-tool' || part.type.startsWith('tool-')) { const name = part.type === 'dynamic-tool' ? String(part.toolName ?? 'tool') : part.type.slice(5); return <AgentToolCard key={index} name={name} label={toolLabels?.[name]} state={String(part.state ?? 'output-available')} input={part.input} output={part.output} inputLabel={labels.input} resultLabel={labels.result} /> }\n return null\n })}</div>\n}\n\nexport function ChatMarkdown({ children }: { children: string }) {\n return <div className=\"space-y-2 text-sm leading-relaxed text-fg\"><Markdown remarkPlugins={[remarkGfm]} components={{ p: ({ children: content }) => <p className=\"whitespace-pre-wrap\">{content}</p>, h1: ({ children: content }) => <h1 className=\"text-lg font-semibold\">{content}</h1>, h2: ({ children: content }) => <h2 className=\"text-base font-semibold\">{content}</h2>, ul: ({ children: content }) => <ul className=\"list-disc space-y-1 pl-5\">{content}</ul>, ol: ({ children: content }) => <ol className=\"list-decimal space-y-1 pl-5\">{content}</ol>, code: ({ children: content }) => <code className=\"rounded bg-bg-subtle px-1 py-0.5 font-mono text-[0.85em] text-primary\">{content}</code>, pre: ({ children: content }) => <pre className=\"overflow-auto rounded-lg bg-overlay p-3 text-sm whitespace-pre-wrap text-white\">{content}</pre>, table: ({ children: content }) => <div className=\"overflow-x-auto\"><table className=\"w-full border-collapse text-sm\">{content}</table></div>, th: ({ children: content }) => <th className=\"border-b border-border px-2 py-1 text-left\">{content}</th>, td: ({ children: content }) => <td className=\"border-b border-border-subtle px-2 py-1\">{content}</td>, a: ({ href, children: content }) => href?.startsWith('/') ? <UiLink href={href} className=\"font-medium text-primary underline-offset-2 hover:underline\">{content}</UiLink> : <a href={href} target=\"_blank\" rel=\"noreferrer\" className=\"font-medium text-primary underline-offset-2 hover:underline\">{content}</a> }}>{children}</Markdown></div>\n}\n\nexport function AgentToolCard({ name, label, state, input, output, inputLabel = 'Input', resultLabel = 'Result' }: { name: string; label?: string; state: string; input?: unknown; output?: unknown; inputLabel?: string; resultLabel?: string }) {\n const [open, setOpen] = React.useState(false)\n const running = state === 'input-streaming' || state === 'input-available'\n const errored = state === 'output-error' || (output as { ok?: boolean } | undefined)?.ok === false\n return <div className=\"overflow-hidden rounded-lg border border-border bg-bg-subtle text-sm\"><button type=\"button\" onClick={() => setOpen((value) => !value)} className=\"flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors hover:bg-surface-hover\"><span className={cn('flex size-6 shrink-0 items-center justify-center rounded-md', errored ? 'bg-danger-subtle text-danger' : 'bg-primary-subtle text-primary')}><Database size={14} /></span><span className=\"min-w-0 flex-1 truncate font-medium text-fg\">{label ?? name.replaceAll('_', ' ')}</span>{running ? <Loader2 size={14} className=\"animate-spin text-fg-subtle\" /> : errored ? <AlertCircle size={14} className=\"text-danger\" /> : <CheckCircle2 size={14} className=\"text-success\" />}<ChevronRight size={14} className={cn('text-fg-subtle transition-transform', open && 'rotate-90')} /></button>{open ? <div className=\"space-y-2 border-t border-border px-3 py-2\">{input !== undefined ? <AgentToolDetail label={inputLabel} value={input} /> : null}{output !== undefined ? <AgentToolDetail label={resultLabel} value={output} /> : null}</div> : null}</div>\n}\n\nfunction AgentToolDetail({ label, value }: { label: string; value: unknown }) {\n let text: string\n try { text = JSON.stringify(value, null, 2) } catch { text = String(value) }\n return <div><div className=\"mb-1 text-[11px] font-semibold tracking-wide text-fg-subtle uppercase\">{label}</div><pre className=\"max-h-60 overflow-auto rounded-md bg-surface p-2 text-xs leading-relaxed text-fg ring-1 ring-border\">{text}</pre></div>\n}\n"]}
|
package/vision.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type AiConfig } from './client.js';
|
|
3
|
+
export declare const photoInsightSchema: z.ZodObject<{
|
|
4
|
+
caption: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type PhotoInsight = z.infer<typeof photoInsightSchema>;
|
|
7
|
+
export declare const AI_VISION_LIMITS: {
|
|
8
|
+
readonly images: 4;
|
|
9
|
+
readonly imageBytes: number;
|
|
10
|
+
readonly totalImageBytes: number;
|
|
11
|
+
readonly promptChars: 4000;
|
|
12
|
+
};
|
|
13
|
+
export declare function assertVisionRequest(args: {
|
|
14
|
+
images: readonly Uint8Array[];
|
|
15
|
+
prompt?: string;
|
|
16
|
+
}): void;
|
|
17
|
+
/**
|
|
18
|
+
* Caption a photo from storage-validated bytes. Returns null when AI
|
|
19
|
+
* is unconfigured.
|
|
20
|
+
*/
|
|
21
|
+
export declare function describePhoto(config: AiConfig | null | undefined, args: {
|
|
22
|
+
image: Uint8Array;
|
|
23
|
+
}): Promise<PhotoInsight | null>;
|
|
24
|
+
export declare const visionSeverity: z.ZodEnum<{
|
|
25
|
+
low: "low";
|
|
26
|
+
medium: "medium";
|
|
27
|
+
high: "high";
|
|
28
|
+
}>;
|
|
29
|
+
export type VisionSeverity = z.infer<typeof visionSeverity>;
|
|
30
|
+
export declare const safetyVisionSchema: z.ZodObject<{
|
|
31
|
+
summary: z.ZodString;
|
|
32
|
+
overallRisk: z.ZodEnum<{
|
|
33
|
+
low: "low";
|
|
34
|
+
medium: "medium";
|
|
35
|
+
high: "high";
|
|
36
|
+
none: "none";
|
|
37
|
+
}>;
|
|
38
|
+
ppe: z.ZodArray<z.ZodObject<{
|
|
39
|
+
item: z.ZodString;
|
|
40
|
+
status: z.ZodEnum<{
|
|
41
|
+
present: "present";
|
|
42
|
+
missing: "missing";
|
|
43
|
+
incorrect: "incorrect";
|
|
44
|
+
}>;
|
|
45
|
+
detail: z.ZodNullable<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>>;
|
|
47
|
+
hazards: z.ZodArray<z.ZodObject<{
|
|
48
|
+
type: z.ZodString;
|
|
49
|
+
severity: z.ZodEnum<{
|
|
50
|
+
low: "low";
|
|
51
|
+
medium: "medium";
|
|
52
|
+
high: "high";
|
|
53
|
+
}>;
|
|
54
|
+
detail: z.ZodString;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export type SafetyVisionAnalysis = z.infer<typeof safetyVisionSchema>;
|
|
58
|
+
/**
|
|
59
|
+
* Review storage-validated jobsite photo bytes for missing PPE + hazards.
|
|
60
|
+
* Returns null when AI is unconfigured or no images are supplied.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runVisionAnalysis(config: AiConfig | null | undefined, args: {
|
|
63
|
+
images: Uint8Array[];
|
|
64
|
+
prompt?: string;
|
|
65
|
+
}): Promise<SafetyVisionAnalysis | null>;
|
|
66
|
+
//# sourceMappingURL=vision.d.ts.map
|
package/vision.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision.d.ts","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAElD,eAAO,MAAM,kBAAkB;;iBAE7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE7D,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAA;AAEV,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,IAAI,CAoBP;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACnC,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuB9B;AAQD,eAAO,MAAM,cAAc;;;;EAAoC,CAAA;AAC/D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAA;AAE3D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8C7B,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAUrE;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACnC,IAAI,EAAE;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAqBtC"}
|
package/vision.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Photo intelligence — a factual auto-caption for an application attachment.
|
|
2
|
+
// Uses the smart, vision-capable model tier.
|
|
3
|
+
import { generateObject } from 'ai';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getModel } from './client.js';
|
|
6
|
+
export const photoInsightSchema = z.object({
|
|
7
|
+
caption: z.string().describe('One factual sentence describing what the photo shows'),
|
|
8
|
+
});
|
|
9
|
+
export const AI_VISION_LIMITS = {
|
|
10
|
+
images: 4,
|
|
11
|
+
imageBytes: 8 * 1024 * 1024,
|
|
12
|
+
totalImageBytes: 10 * 1024 * 1024,
|
|
13
|
+
promptChars: 4_000,
|
|
14
|
+
};
|
|
15
|
+
export function assertVisionRequest(args) {
|
|
16
|
+
if (args.images.length === 0 || args.images.length > AI_VISION_LIMITS.images) {
|
|
17
|
+
throw new Error(`AI vision requires between 1 and ${AI_VISION_LIMITS.images} images.`);
|
|
18
|
+
}
|
|
19
|
+
if (args.prompt !== undefined && args.prompt.length > AI_VISION_LIMITS.promptChars) {
|
|
20
|
+
throw new Error(`AI vision prompt exceeds ${AI_VISION_LIMITS.promptChars} characters.`);
|
|
21
|
+
}
|
|
22
|
+
let totalBytes = 0;
|
|
23
|
+
for (const image of args.images) {
|
|
24
|
+
if (!(image instanceof Uint8Array) || image.byteLength === 0) {
|
|
25
|
+
throw new Error('AI vision image must contain bytes.');
|
|
26
|
+
}
|
|
27
|
+
if (image.byteLength > AI_VISION_LIMITS.imageBytes) {
|
|
28
|
+
throw new Error(`AI vision image exceeds ${AI_VISION_LIMITS.imageBytes} bytes.`);
|
|
29
|
+
}
|
|
30
|
+
totalBytes += image.byteLength;
|
|
31
|
+
if (totalBytes > AI_VISION_LIMITS.totalImageBytes) {
|
|
32
|
+
throw new Error(`AI vision images exceed ${AI_VISION_LIMITS.totalImageBytes} total bytes.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Caption a photo from storage-validated bytes. Returns null when AI
|
|
38
|
+
* is unconfigured.
|
|
39
|
+
*/
|
|
40
|
+
export async function describePhoto(config, args) {
|
|
41
|
+
const model = getModel(config, 'smart');
|
|
42
|
+
if (!model)
|
|
43
|
+
return null;
|
|
44
|
+
assertVisionRequest({ images: [args.image] });
|
|
45
|
+
const { object } = await generateObject({
|
|
46
|
+
model,
|
|
47
|
+
schema: photoInsightSchema,
|
|
48
|
+
messages: [
|
|
49
|
+
{
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: 'Write a one-sentence factual caption for this photo. Describe only what is shown; do not speculate.',
|
|
55
|
+
},
|
|
56
|
+
{ type: 'image', image: args.image },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
temperature: 0.2,
|
|
61
|
+
});
|
|
62
|
+
return object;
|
|
63
|
+
}
|
|
64
|
+
// --- Safety vision analysis -------------------------------------------------
|
|
65
|
+
//
|
|
66
|
+
// Construction H&S review of one or more jobsite photos: flags missing/incorrect
|
|
67
|
+
// PPE and visible hazards with a severity. Powers the `photo_ai` fill element +
|
|
68
|
+
// the `analyze_photos` flow action. Uses the smart (vision) tier.
|
|
69
|
+
export const visionSeverity = z.enum(['low', 'medium', 'high']);
|
|
70
|
+
export const safetyVisionSchema = z.object({
|
|
71
|
+
summary: z
|
|
72
|
+
.string()
|
|
73
|
+
.describe('One or two factual sentences describing the photo from a health & safety standpoint'),
|
|
74
|
+
overallRisk: z
|
|
75
|
+
.enum(['none', 'low', 'medium', 'high'])
|
|
76
|
+
.describe('Overall risk level visible in the photo — "none" if nothing of concern is visible'),
|
|
77
|
+
ppe: z
|
|
78
|
+
.array(z.object({
|
|
79
|
+
item: z
|
|
80
|
+
.string()
|
|
81
|
+
.describe('PPE item, e.g. "hard hat", "hi-vis vest", "eye protection", "gloves", "fall harness"'),
|
|
82
|
+
status: z
|
|
83
|
+
.enum(['present', 'missing', 'incorrect'])
|
|
84
|
+
.describe('Whether this PPE is correctly worn, missing, or worn incorrectly'),
|
|
85
|
+
detail: z
|
|
86
|
+
.string()
|
|
87
|
+
.nullable()
|
|
88
|
+
.describe('Short note locating the observation, e.g. "worker on the right has no hard hat"'),
|
|
89
|
+
}))
|
|
90
|
+
.max(12)
|
|
91
|
+
.describe('PPE observations for people visible in the photo. Empty when no people are visible.'),
|
|
92
|
+
hazards: z
|
|
93
|
+
.array(z.object({
|
|
94
|
+
type: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe('Hazard category, e.g. "working at height", "trip hazard", "exposed edge", "electrical", "housekeeping"'),
|
|
97
|
+
severity: visionSeverity,
|
|
98
|
+
detail: z.string().describe('What was observed and where in the photo'),
|
|
99
|
+
}))
|
|
100
|
+
.max(12)
|
|
101
|
+
.describe('Hazards / unsafe conditions clearly visible in the photo'),
|
|
102
|
+
});
|
|
103
|
+
const SAFETY_VISION_PROMPT = `You are an experienced construction health & safety inspector reviewing jobsite photo(s).
|
|
104
|
+
Report only what is CLEARLY VISIBLE — never speculate about what might be out of frame.
|
|
105
|
+
|
|
106
|
+
1. PPE: for each person visible, assess required PPE (hard hat, hi-vis, eye protection, gloves, appropriate footwear, and fall protection when working at height). Mark each as present, missing, or incorrect, and say which person.
|
|
107
|
+
2. Hazards: identify unsafe conditions (unprotected work at height, trip/slip hazards, exposed edges/openings, poor housekeeping, electrical, struck-by, etc.) with a severity.
|
|
108
|
+
|
|
109
|
+
If no people are visible, return an empty ppe list. If nothing of concern is visible, return empty hazards and overallRisk "none". Set overallRisk to the highest individual concern.`;
|
|
110
|
+
/**
|
|
111
|
+
* Review storage-validated jobsite photo bytes for missing PPE + hazards.
|
|
112
|
+
* Returns null when AI is unconfigured or no images are supplied.
|
|
113
|
+
*/
|
|
114
|
+
export async function runVisionAnalysis(config, args) {
|
|
115
|
+
if (!args.images || args.images.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
const model = getModel(config, 'smart');
|
|
118
|
+
if (!model)
|
|
119
|
+
return null;
|
|
120
|
+
assertVisionRequest(args);
|
|
121
|
+
const { object } = await generateObject({
|
|
122
|
+
model,
|
|
123
|
+
schema: safetyVisionSchema,
|
|
124
|
+
messages: [
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: [
|
|
128
|
+
{ type: 'text', text: args.prompt ?? SAFETY_VISION_PROMPT },
|
|
129
|
+
...args.images.map((image) => ({ type: 'image', image })),
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
temperature: 0.2,
|
|
134
|
+
});
|
|
135
|
+
return object;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=vision.js.map
|
package/vision.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision.js","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"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","sourcesContent":["// Photo intelligence — a factual auto-caption for an application attachment.\n// Uses the smart, vision-capable model tier.\n\nimport { generateObject } from 'ai'\nimport { z } from 'zod'\nimport { getModel, type AiConfig } from './client'\n\nexport const photoInsightSchema = z.object({\n caption: z.string().describe('One factual sentence describing what the photo shows'),\n})\n\nexport type PhotoInsight = z.infer<typeof photoInsightSchema>\n\nexport const AI_VISION_LIMITS = {\n images: 4,\n imageBytes: 8 * 1024 * 1024,\n totalImageBytes: 10 * 1024 * 1024,\n promptChars: 4_000,\n} as const\n\nexport function assertVisionRequest(args: {\n images: readonly Uint8Array[]\n prompt?: string\n}): void {\n if (args.images.length === 0 || args.images.length > AI_VISION_LIMITS.images) {\n throw new Error(`AI vision requires between 1 and ${AI_VISION_LIMITS.images} images.`)\n }\n if (args.prompt !== undefined && args.prompt.length > AI_VISION_LIMITS.promptChars) {\n throw new Error(`AI vision prompt exceeds ${AI_VISION_LIMITS.promptChars} characters.`)\n }\n let totalBytes = 0\n for (const image of args.images) {\n if (!(image instanceof Uint8Array) || image.byteLength === 0) {\n throw new Error('AI vision image must contain bytes.')\n }\n if (image.byteLength > AI_VISION_LIMITS.imageBytes) {\n throw new Error(`AI vision image exceeds ${AI_VISION_LIMITS.imageBytes} bytes.`)\n }\n totalBytes += image.byteLength\n if (totalBytes > AI_VISION_LIMITS.totalImageBytes) {\n throw new Error(`AI vision images exceed ${AI_VISION_LIMITS.totalImageBytes} total bytes.`)\n }\n }\n}\n\n/**\n * Caption a photo from storage-validated bytes. Returns null when AI\n * is unconfigured.\n */\nexport async function describePhoto(\n config: AiConfig | null | undefined,\n args: { image: Uint8Array },\n): Promise<PhotoInsight | null> {\n const model = getModel(config, 'smart')\n if (!model) return null\n assertVisionRequest({ images: [args.image] })\n\n const { object } = await generateObject({\n model,\n schema: photoInsightSchema,\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: 'Write a one-sentence factual caption for this photo. Describe only what is shown; do not speculate.',\n },\n { type: 'image', image: args.image },\n ],\n },\n ],\n temperature: 0.2,\n })\n return object\n}\n\n// --- Safety vision analysis -------------------------------------------------\n//\n// Construction H&S review of one or more jobsite photos: flags missing/incorrect\n// PPE and visible hazards with a severity. Powers the `photo_ai` fill element +\n// the `analyze_photos` flow action. Uses the smart (vision) tier.\n\nexport const visionSeverity = z.enum(['low', 'medium', 'high'])\nexport type VisionSeverity = z.infer<typeof visionSeverity>\n\nexport const safetyVisionSchema = z.object({\n summary: z\n .string()\n .describe(\n 'One or two factual sentences describing the photo from a health & safety standpoint',\n ),\n overallRisk: z\n .enum(['none', 'low', 'medium', 'high'])\n .describe('Overall risk level visible in the photo — \"none\" if nothing of concern is visible'),\n ppe: z\n .array(\n z.object({\n item: z\n .string()\n .describe(\n 'PPE item, e.g. \"hard hat\", \"hi-vis vest\", \"eye protection\", \"gloves\", \"fall harness\"',\n ),\n status: z\n .enum(['present', 'missing', 'incorrect'])\n .describe('Whether this PPE is correctly worn, missing, or worn incorrectly'),\n detail: z\n .string()\n .nullable()\n .describe(\n 'Short note locating the observation, e.g. \"worker on the right has no hard hat\"',\n ),\n }),\n )\n .max(12)\n .describe(\n 'PPE observations for people visible in the photo. Empty when no people are visible.',\n ),\n hazards: z\n .array(\n z.object({\n type: z\n .string()\n .describe(\n 'Hazard category, e.g. \"working at height\", \"trip hazard\", \"exposed edge\", \"electrical\", \"housekeeping\"',\n ),\n severity: visionSeverity,\n detail: z.string().describe('What was observed and where in the photo'),\n }),\n )\n .max(12)\n .describe('Hazards / unsafe conditions clearly visible in the photo'),\n})\n\nexport type SafetyVisionAnalysis = z.infer<typeof safetyVisionSchema>\n\nconst SAFETY_VISION_PROMPT = `You are an experienced construction health & safety inspector reviewing jobsite photo(s).\nReport only what is CLEARLY VISIBLE — never speculate about what might be out of frame.\n\n1. PPE: for each person visible, assess required PPE (hard hat, hi-vis, eye protection, gloves, appropriate footwear, and fall protection when working at height). Mark each as present, missing, or incorrect, and say which person.\n2. Hazards: identify unsafe conditions (unprotected work at height, trip/slip hazards, exposed edges/openings, poor housekeeping, electrical, struck-by, etc.) with a severity.\n\nIf no people are visible, return an empty ppe list. If nothing of concern is visible, return empty hazards and overallRisk \"none\". Set overallRisk to the highest individual concern.`\n\n/**\n * Review storage-validated jobsite photo bytes for missing PPE + hazards.\n * Returns null when AI is unconfigured or no images are supplied.\n */\nexport async function runVisionAnalysis(\n config: AiConfig | null | undefined,\n args: { images: Uint8Array[]; prompt?: string },\n): Promise<SafetyVisionAnalysis | null> {\n if (!args.images || args.images.length === 0) return null\n const model = getModel(config, 'smart')\n if (!model) return null\n assertVisionRequest(args)\n\n const { object } = await generateObject({\n model,\n schema: safetyVisionSchema,\n messages: [\n {\n role: 'user',\n content: [\n { type: 'text', text: args.prompt ?? SAFETY_VISION_PROMPT },\n ...args.images.map((image) => ({ type: 'image' as const, image })),\n ],\n },\n ],\n temperature: 0.2,\n })\n return object\n}\n"]}
|
package/writing.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AiConfig } from './client.js';
|
|
2
|
+
export type WritingMode = 'tidy' | 'expand' | 'continue' | 'rephrase' | 'fix' | 'bulletize' | 'summarize';
|
|
3
|
+
export declare const WRITING_MODES: WritingMode[];
|
|
4
|
+
export declare function isWritingMode(v: string): v is WritingMode;
|
|
5
|
+
/**
|
|
6
|
+
* Stream a writing-assist transformation as a plain text-stream Response. Throws
|
|
7
|
+
* AIDisabledError when the tenant has no model configured.
|
|
8
|
+
*/
|
|
9
|
+
export declare function streamWritingAssist(config: AiConfig | null | undefined, args: {
|
|
10
|
+
mode: WritingMode;
|
|
11
|
+
text: string;
|
|
12
|
+
tone?: string;
|
|
13
|
+
context?: string;
|
|
14
|
+
}): Response;
|
|
15
|
+
//# sourceMappingURL=writing.d.ts.map
|
package/writing.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writing.d.ts","sourceRoot":"","sources":["../src/writing.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAGnE,MAAM,MAAM,WAAW,GACrB,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,KAAK,GAAG,WAAW,GAAG,WAAW,CAAA;AAEjF,eAAO,MAAM,aAAa,EAAE,WAAW,EAQtC,CAAA;AAgBD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,WAAW,CAEzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACnC,IAAI,EAAE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE,QAAQ,CAmBV"}
|
package/writing.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Inline writing assist — streamed token-by-token into the editor.
|
|
2
|
+
import { streamText } from 'ai';
|
|
3
|
+
import { AIDisabledError, getModel } from './client.js';
|
|
4
|
+
import { ENTRY_WRITING_SYSTEM, orgContextLine } from './prompts.js';
|
|
5
|
+
export const WRITING_MODES = [
|
|
6
|
+
'tidy',
|
|
7
|
+
'expand',
|
|
8
|
+
'continue',
|
|
9
|
+
'rephrase',
|
|
10
|
+
'fix',
|
|
11
|
+
'bulletize',
|
|
12
|
+
'summarize',
|
|
13
|
+
];
|
|
14
|
+
const INSTRUCTIONS = {
|
|
15
|
+
tidy: 'Rewrite the text below so it reads clearly and professionally, preserving every fact. Fix grammar and flow. Keep it roughly the same length.',
|
|
16
|
+
expand: 'Expand the brief notes below into a fuller entry. Add natural connective wording, but NEVER invent specific facts (names, numbers, events) that are not present or clearly implied.',
|
|
17
|
+
continue: 'Continue the entry below in the same voice for one to three more sentences. Output only the continuation.',
|
|
18
|
+
rephrase: 'Rephrase the text below{tone}, preserving the meaning and all facts.',
|
|
19
|
+
fix: 'Correct spelling, grammar and punctuation in the text below. Change nothing else.',
|
|
20
|
+
bulletize: 'Convert the text below into a tight bulleted list covering the key activity, observations, and follow-up actions. Use "- " bullets and no preamble.',
|
|
21
|
+
summarize: 'Summarize the text below into one short paragraph capturing the key activity, observations, and actions.',
|
|
22
|
+
};
|
|
23
|
+
export function isWritingMode(v) {
|
|
24
|
+
return WRITING_MODES.includes(v);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stream a writing-assist transformation as a plain text-stream Response. Throws
|
|
28
|
+
* AIDisabledError when the tenant has no model configured.
|
|
29
|
+
*/
|
|
30
|
+
export function streamWritingAssist(config, args) {
|
|
31
|
+
const model = getModel(config, 'fast');
|
|
32
|
+
if (!model)
|
|
33
|
+
throw new AIDisabledError();
|
|
34
|
+
const instruction = INSTRUCTIONS[args.mode].replace('{tone}', args.tone ? ` in a ${args.tone} tone` : '');
|
|
35
|
+
const context = args.context
|
|
36
|
+
? `\n\nContext (background only, do not repeat verbatim): ${args.context}`
|
|
37
|
+
: '';
|
|
38
|
+
const result = streamText({
|
|
39
|
+
model,
|
|
40
|
+
system: ENTRY_WRITING_SYSTEM + orgContextLine(config?.org),
|
|
41
|
+
prompt: `${instruction}${context}\n\n---\n${args.text}`,
|
|
42
|
+
temperature: 0.4,
|
|
43
|
+
});
|
|
44
|
+
return result.toTextStreamResponse();
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=writing.js.map
|
package/writing.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writing.js","sourceRoot":"","sources":["../src/writing.ts"],"names":[],"mappings":"AAAA,mEAAmE;AAEnE,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC/B,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAiB,MAAM,UAAU,CAAA;AACnE,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAKhE,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,MAAM;IACN,QAAQ;IACR,UAAU;IACV,UAAU;IACV,KAAK;IACL,WAAW;IACX,WAAW;CACZ,CAAA;AAED,MAAM,YAAY,GAAgC;IAChD,IAAI,EAAE,8IAA8I;IACpJ,MAAM,EACJ,qLAAqL;IACvL,QAAQ,EACN,2GAA2G;IAC7G,QAAQ,EAAE,sEAAsE;IAChF,GAAG,EAAE,mFAAmF;IACxF,SAAS,EACP,qJAAqJ;IACvJ,SAAS,EACP,0GAA0G;CAC7G,CAAA;AAED,MAAM,UAAU,aAAa,CAAC,CAAS;IACrC,OAAQ,aAA0B,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAmC,EACnC,IAA0E;IAE1E,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,eAAe,EAAE,CAAA;IAEvC,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CACjD,QAAQ,EACR,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAC3C,CAAA;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO;QAC1B,CAAC,CAAC,0DAA0D,IAAI,CAAC,OAAO,EAAE;QAC1E,CAAC,CAAC,EAAE,CAAA;IAEN,MAAM,MAAM,GAAG,UAAU,CAAC;QACxB,KAAK;QACL,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC;QAC1D,MAAM,EAAE,GAAG,WAAW,GAAG,OAAO,YAAY,IAAI,CAAC,IAAI,EAAE;QACvD,WAAW,EAAE,GAAG;KACjB,CAAC,CAAA;IACF,OAAO,MAAM,CAAC,oBAAoB,EAAE,CAAA;AACtC,CAAC","sourcesContent":["// Inline writing assist — streamed token-by-token into the editor.\n\nimport { streamText } from 'ai'\nimport { AIDisabledError, getModel, type AiConfig } from './client'\nimport { ENTRY_WRITING_SYSTEM, orgContextLine } from './prompts'\n\nexport type WritingMode =\n 'tidy' | 'expand' | 'continue' | 'rephrase' | 'fix' | 'bulletize' | 'summarize'\n\nexport const WRITING_MODES: WritingMode[] = [\n 'tidy',\n 'expand',\n 'continue',\n 'rephrase',\n 'fix',\n 'bulletize',\n 'summarize',\n]\n\nconst INSTRUCTIONS: Record<WritingMode, string> = {\n tidy: 'Rewrite the text below so it reads clearly and professionally, preserving every fact. Fix grammar and flow. Keep it roughly the same length.',\n expand:\n 'Expand the brief notes below into a fuller entry. Add natural connective wording, but NEVER invent specific facts (names, numbers, events) that are not present or clearly implied.',\n continue:\n 'Continue the entry below in the same voice for one to three more sentences. Output only the continuation.',\n rephrase: 'Rephrase the text below{tone}, preserving the meaning and all facts.',\n fix: 'Correct spelling, grammar and punctuation in the text below. Change nothing else.',\n bulletize:\n 'Convert the text below into a tight bulleted list covering the key activity, observations, and follow-up actions. Use \"- \" bullets and no preamble.',\n summarize:\n 'Summarize the text below into one short paragraph capturing the key activity, observations, and actions.',\n}\n\nexport function isWritingMode(v: string): v is WritingMode {\n return (WRITING_MODES as string[]).includes(v)\n}\n\n/**\n * Stream a writing-assist transformation as a plain text-stream Response. Throws\n * AIDisabledError when the tenant has no model configured.\n */\nexport function streamWritingAssist(\n config: AiConfig | null | undefined,\n args: { mode: WritingMode; text: string; tone?: string; context?: string },\n): Response {\n const model = getModel(config, 'fast')\n if (!model) throw new AIDisabledError()\n\n const instruction = INSTRUCTIONS[args.mode].replace(\n '{tone}',\n args.tone ? ` in a ${args.tone} tone` : '',\n )\n const context = args.context\n ? `\\n\\nContext (background only, do not repeat verbatim): ${args.context}`\n : ''\n\n const result = streamText({\n model,\n system: ENTRY_WRITING_SYSTEM + orgContextLine(config?.org),\n prompt: `${instruction}${context}\\n\\n---\\n${args.text}`,\n temperature: 0.4,\n })\n return result.toTextStreamResponse()\n}\n"]}
|