gigaclaw 1.4.0
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 +26 -0
- package/README.md +237 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +265 -0
- package/bin/cli.js +823 -0
- package/bin/local.sh +85 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +26 -0
- package/config/instrumentation.js +62 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_nostalgic_sersi.sql +11 -0
- package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
- package/drizzle/0003_rename_code_workspaces.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0001_snapshot.json +390 -0
- package/drizzle/meta/0002_snapshot.json +411 -0
- package/drizzle/meta/0003_snapshot.json +419 -0
- package/drizzle/meta/_journal.json +34 -0
- package/lib/actions.js +44 -0
- package/lib/ai/agent.js +86 -0
- package/lib/ai/index.js +342 -0
- package/lib/ai/model.js +180 -0
- package/lib/ai/tools.js +269 -0
- package/lib/ai/web-search.js +42 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +62 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +579 -0
- package/lib/chat/api.js +140 -0
- package/lib/chat/components/app-sidebar.js +213 -0
- package/lib/chat/components/app-sidebar.jsx +279 -0
- package/lib/chat/components/chat-header.js +192 -0
- package/lib/chat/components/chat-header.jsx +223 -0
- package/lib/chat/components/chat-input.js +236 -0
- package/lib/chat/components/chat-input.jsx +249 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +99 -0
- package/lib/chat/components/chat-page.jsx +121 -0
- package/lib/chat/components/chat.js +153 -0
- package/lib/chat/components/chat.jsx +199 -0
- package/lib/chat/components/chats-page.js +367 -0
- package/lib/chat/components/chats-page.jsx +394 -0
- package/lib/chat/components/code-mode-toggle.js +132 -0
- package/lib/chat/components/code-mode-toggle.jsx +163 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +16 -0
- package/lib/chat/components/icons.js +805 -0
- package/lib/chat/components/icons.jsx +751 -0
- package/lib/chat/components/index.js +20 -0
- package/lib/chat/components/message.js +363 -0
- package/lib/chat/components/message.jsx +422 -0
- package/lib/chat/components/messages.js +65 -0
- package/lib/chat/components/messages.jsx +74 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/pull-requests-page.js +103 -0
- package/lib/chat/components/pull-requests-page.jsx +113 -0
- package/lib/chat/components/settings-layout.js +39 -0
- package/lib/chat/components/settings-layout.jsx +53 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +138 -0
- package/lib/chat/components/sidebar-history-item.jsx +119 -0
- package/lib/chat/components/sidebar-history.js +167 -0
- package/lib/chat/components/sidebar-history.jsx +220 -0
- package/lib/chat/components/sidebar-user-nav.js +61 -0
- package/lib/chat/components/sidebar-user-nav.jsx +77 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/tool-call.js +89 -0
- package/lib/chat/components/tool-call.jsx +107 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/combobox.js +98 -0
- package/lib/chat/components/ui/combobox.jsx +114 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +194 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
- package/lib/chat/components/ui/rename-dialog.js +78 -0
- package/lib/chat/components/ui/rename-dialog.jsx +74 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +228 -0
- package/lib/chat/components/ui/sidebar.jsx +246 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/code/actions.js +153 -0
- package/lib/code/code-page.js +22 -0
- package/lib/code/code-page.jsx +25 -0
- package/lib/code/index.js +1 -0
- package/lib/code/terminal-view.js +201 -0
- package/lib/code/terminal-view.jsx +224 -0
- package/lib/code/ws-proxy.js +80 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +168 -0
- package/lib/db/code-workspaces.js +110 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +66 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +42 -0
- package/lib/tools/create-job.js +97 -0
- package/lib/tools/docker.js +146 -0
- package/lib/tools/github.js +271 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +104 -0
- package/lib/utils/render-md.js +111 -0
- package/package.json +118 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +105 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-cloud.mjs +833 -0
- package/setup/setup-local.mjs +377 -0
- package/setup/setup-telegram.mjs +265 -0
- package/setup/setup.mjs +87 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +104 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +308 -0
- package/templates/app/api/[...gigaclaw]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +9 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
- package/templates/app/components/ascii-logo.jsx +12 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +33 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/pull-requests/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/CODE_PLANNING.md +14 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_AGENT.md +30 -0
- package/templates/config/JOB_PLANNING.md +240 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
- package/templates/config/SOUL.md +48 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
- package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
- package/templates/docker/claude-code-job/Dockerfile +34 -0
- package/templates/docker/claude-code-job/entrypoint.sh +149 -0
- package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
- package/templates/docker/claude-code-workspace/Dockerfile +61 -0
- package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
- package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
- package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
- package/templates/docker-compose.local.yml +78 -0
- package/templates/docker-compose.yml +64 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +23 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/server.js +25 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/google-docs/SKILL.md +23 -0
- package/templates/skills/google-docs/create.sh +69 -0
- package/templates/skills/google-drive/SKILL.md +47 -0
- package/templates/skills/google-drive/delete.sh +47 -0
- package/templates/skills/google-drive/download.sh +50 -0
- package/templates/skills/google-drive/list.sh +41 -0
- package/templates/skills/google-drive/upload.sh +76 -0
- package/templates/skills/kie-ai/SKILL.md +38 -0
- package/templates/skills/kie-ai/generate-image.sh +77 -0
- package/templates/skills/kie-ai/generate-video.sh +69 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
- package/templates/skills/youtube-transcript/SKILL.md +41 -0
- package/templates/skills/youtube-transcript/package-lock.json +24 -0
- package/templates/skills/youtube-transcript/package.json +8 -0
- package/templates/skills/youtube-transcript/transcript.js +84 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { Streamdown } from 'streamdown';
|
|
5
|
+
import { cn } from '../utils.js';
|
|
6
|
+
import { SpinnerIcon, FileTextIcon, CopyIcon, CheckIcon, RefreshIcon, SquarePenIcon, WrenchIcon, XIcon, ChevronDownIcon } from './icons.js';
|
|
7
|
+
|
|
8
|
+
function LinkSafetyModal({ url, isOpen, onClose, onConfirm }) {
|
|
9
|
+
const [copied, setCopied] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleCopy = useCallback(async () => {
|
|
12
|
+
try {
|
|
13
|
+
await navigator.clipboard.writeText(url);
|
|
14
|
+
setCopied(true);
|
|
15
|
+
setTimeout(() => setCopied(false), 2000);
|
|
16
|
+
} catch {}
|
|
17
|
+
}, [url]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isOpen) return;
|
|
21
|
+
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
|
22
|
+
document.addEventListener('keydown', onKey);
|
|
23
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
24
|
+
}, [isOpen, onClose]);
|
|
25
|
+
|
|
26
|
+
if (!isOpen) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
|
31
|
+
onClick={onClose}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
className="relative mx-4 flex w-full flex-col gap-3 rounded-lg border border-border bg-background p-4 shadow-lg"
|
|
35
|
+
style={{ maxWidth: '340px' }}
|
|
36
|
+
onClick={(e) => e.stopPropagation()}
|
|
37
|
+
>
|
|
38
|
+
<div className="font-medium text-sm text-foreground">Open external link?</div>
|
|
39
|
+
<div className="break-all rounded bg-muted px-2.5 py-2 font-mono text-xs text-foreground">
|
|
40
|
+
{url}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex gap-2">
|
|
43
|
+
<button
|
|
44
|
+
onClick={handleCopy}
|
|
45
|
+
className="flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted"
|
|
46
|
+
>
|
|
47
|
+
{copied ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
|
48
|
+
<span>{copied ? 'Copied' : 'Copy'}</span>
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => { onConfirm(); onClose(); }}
|
|
52
|
+
className="flex flex-1 items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90"
|
|
53
|
+
>
|
|
54
|
+
<span>Open</span>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const linkSafety = {
|
|
63
|
+
enabled: true,
|
|
64
|
+
renderModal: (props) => <LinkSafetyModal {...props} />,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const TOOL_DISPLAY_NAMES = {
|
|
68
|
+
create_job: 'Create Job',
|
|
69
|
+
get_job_status: 'Check Job Status',
|
|
70
|
+
get_system_technical_specs: 'Read Tech Docs',
|
|
71
|
+
get_skill_building_guide: 'Read Skill Docs',
|
|
72
|
+
start_coding: 'Start Coding',
|
|
73
|
+
get_repository_details: 'Get Repository Details',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function getToolDisplayName(toolName) {
|
|
77
|
+
return TOOL_DISPLAY_NAMES[toolName] || toolName.replace(/_/g, ' ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatContent(content) {
|
|
81
|
+
if (content == null) return null;
|
|
82
|
+
if (typeof content === 'string') {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(content);
|
|
85
|
+
return JSON.stringify(parsed, null, 2);
|
|
86
|
+
} catch {
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(content, null, 2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ToolCall({ part }) {
|
|
94
|
+
const [expanded, setExpanded] = useState(false);
|
|
95
|
+
|
|
96
|
+
const toolName = part.toolName || (part.type?.startsWith('tool-') ? part.type.slice(5) : 'tool');
|
|
97
|
+
const displayName = getToolDisplayName(toolName);
|
|
98
|
+
const state = part.state || 'input-available';
|
|
99
|
+
|
|
100
|
+
const isRunning = state === 'input-streaming' || state === 'input-available';
|
|
101
|
+
const isDone = state === 'output-available';
|
|
102
|
+
const isError = state === 'output-error';
|
|
103
|
+
|
|
104
|
+
// Auto-redirect when start_coding completes successfully.
|
|
105
|
+
// mountedDone captures whether the tool was already finished when the component
|
|
106
|
+
// first rendered (i.e. the user is revisiting a chat). In that case we skip the
|
|
107
|
+
// redirect so they can still read the conversation.
|
|
108
|
+
const mountedDone = useRef(isDone);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (toolName !== 'start_coding' || !isDone || mountedDone.current) return;
|
|
111
|
+
try {
|
|
112
|
+
const output = typeof part.output === 'string' ? JSON.parse(part.output) : part.output;
|
|
113
|
+
if (output?.success && output?.workspaceUrl) {
|
|
114
|
+
window.location.href = output.workspaceUrl;
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
}, [toolName, isDone, part.output]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="my-1 rounded-lg border border-border bg-background">
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => setExpanded(!expanded)}
|
|
123
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted/50 rounded-lg"
|
|
124
|
+
>
|
|
125
|
+
<WrenchIcon size={14} className="text-muted-foreground shrink-0" />
|
|
126
|
+
<span className="font-medium text-foreground">{displayName}</span>
|
|
127
|
+
<span className="ml-auto flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
128
|
+
{isRunning && (
|
|
129
|
+
<>
|
|
130
|
+
<SpinnerIcon size={12} />
|
|
131
|
+
<span>Running...</span>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
{isDone && (
|
|
135
|
+
<>
|
|
136
|
+
<CheckIcon size={12} className="text-green-500" />
|
|
137
|
+
<span>Done</span>
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
{isError && (
|
|
141
|
+
<>
|
|
142
|
+
<XIcon size={12} className="text-red-500" />
|
|
143
|
+
<span>Error</span>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
</span>
|
|
147
|
+
<ChevronDownIcon
|
|
148
|
+
size={14}
|
|
149
|
+
className={cn(
|
|
150
|
+
'text-muted-foreground transition-transform shrink-0',
|
|
151
|
+
expanded && 'rotate-180'
|
|
152
|
+
)}
|
|
153
|
+
/>
|
|
154
|
+
</button>
|
|
155
|
+
|
|
156
|
+
{expanded && (
|
|
157
|
+
<div className="border-t border-border px-3 py-2 text-xs">
|
|
158
|
+
{part.input != null && (
|
|
159
|
+
<div className="mb-2">
|
|
160
|
+
<div className="font-medium text-muted-foreground mb-1">Input</div>
|
|
161
|
+
<pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto">
|
|
162
|
+
{formatContent(part.input)}
|
|
163
|
+
</pre>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
{part.output != null && (
|
|
167
|
+
<div>
|
|
168
|
+
<div className="font-medium text-muted-foreground mb-1">Output</div>
|
|
169
|
+
<pre className="whitespace-pre-wrap break-all rounded bg-muted p-2 text-foreground overflow-x-auto max-h-64 overflow-y-auto">
|
|
170
|
+
{formatContent(part.output)}
|
|
171
|
+
</pre>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
{part.input == null && part.output == null && (
|
|
175
|
+
<div className="text-muted-foreground italic">Waiting for data...</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
|
|
184
|
+
const isUser = message.role === 'user';
|
|
185
|
+
const [copied, setCopied] = useState(false);
|
|
186
|
+
const [editing, setEditing] = useState(false);
|
|
187
|
+
const [editText, setEditText] = useState('');
|
|
188
|
+
const textareaRef = useRef(null);
|
|
189
|
+
|
|
190
|
+
// Extract text from parts (AI SDK v5+) or fall back to content
|
|
191
|
+
const text =
|
|
192
|
+
message.parts
|
|
193
|
+
?.filter((p) => p.type === 'text')
|
|
194
|
+
.map((p) => p.text)
|
|
195
|
+
.join('\n') ||
|
|
196
|
+
message.content ||
|
|
197
|
+
'';
|
|
198
|
+
|
|
199
|
+
// Extract file parts
|
|
200
|
+
const fileParts = message.parts?.filter((p) => p.type === 'file') || [];
|
|
201
|
+
const imageParts = fileParts.filter((p) => p.mediaType?.startsWith('image/'));
|
|
202
|
+
const otherFileParts = fileParts.filter((p) => !p.mediaType?.startsWith('image/'));
|
|
203
|
+
const hasToolParts = message.parts?.some((p) => p.type?.startsWith('tool-')) || false;
|
|
204
|
+
|
|
205
|
+
const handleCopy = async () => {
|
|
206
|
+
try {
|
|
207
|
+
await navigator.clipboard.writeText(text);
|
|
208
|
+
setCopied(true);
|
|
209
|
+
setTimeout(() => setCopied(false), 2000);
|
|
210
|
+
} catch {}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleEditStart = () => {
|
|
214
|
+
setEditText(text);
|
|
215
|
+
setEditing(true);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleEditCancel = () => {
|
|
219
|
+
setEditing(false);
|
|
220
|
+
setEditText('');
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleEditSubmit = () => {
|
|
224
|
+
const trimmed = editText.trim();
|
|
225
|
+
if (trimmed && trimmed !== text) {
|
|
226
|
+
onEdit?.(message, trimmed);
|
|
227
|
+
}
|
|
228
|
+
setEditing(false);
|
|
229
|
+
setEditText('');
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Auto-resize and focus textarea when entering edit mode
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (editing && textareaRef.current) {
|
|
235
|
+
const ta = textareaRef.current;
|
|
236
|
+
ta.focus();
|
|
237
|
+
ta.style.height = 'auto';
|
|
238
|
+
ta.style.height = `${ta.scrollHeight}px`;
|
|
239
|
+
// Move cursor to end
|
|
240
|
+
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
241
|
+
}
|
|
242
|
+
}, [editing]);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div
|
|
246
|
+
className={cn(
|
|
247
|
+
'group flex gap-4 w-full',
|
|
248
|
+
isUser ? 'justify-end' : 'justify-start'
|
|
249
|
+
)}
|
|
250
|
+
>
|
|
251
|
+
<div className={cn('flex flex-col', isUser ? 'max-w-[80%]' : 'w-full')}>
|
|
252
|
+
{editing ? (
|
|
253
|
+
<div className="flex flex-col gap-2">
|
|
254
|
+
<textarea
|
|
255
|
+
ref={textareaRef}
|
|
256
|
+
value={editText}
|
|
257
|
+
onChange={(e) => {
|
|
258
|
+
setEditText(e.target.value);
|
|
259
|
+
e.target.style.height = 'auto';
|
|
260
|
+
e.target.style.height = `${e.target.scrollHeight}px`;
|
|
261
|
+
}}
|
|
262
|
+
onKeyDown={(e) => {
|
|
263
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
handleEditSubmit();
|
|
266
|
+
}
|
|
267
|
+
if (e.key === 'Escape') {
|
|
268
|
+
handleEditCancel();
|
|
269
|
+
}
|
|
270
|
+
}}
|
|
271
|
+
className="w-full resize-none rounded-xl border border-border bg-muted px-4 py-3 text-sm leading-relaxed text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
272
|
+
rows={1}
|
|
273
|
+
/>
|
|
274
|
+
<div className="flex justify-end gap-2">
|
|
275
|
+
<button
|
|
276
|
+
onClick={handleEditCancel}
|
|
277
|
+
className="rounded-md px-3 py-1 text-xs text-muted-foreground hover:text-foreground"
|
|
278
|
+
>
|
|
279
|
+
Cancel
|
|
280
|
+
</button>
|
|
281
|
+
<button
|
|
282
|
+
onClick={handleEditSubmit}
|
|
283
|
+
className="rounded-md bg-primary px-3 py-1 text-xs text-primary-foreground hover:opacity-80"
|
|
284
|
+
>
|
|
285
|
+
Send
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<>
|
|
291
|
+
<div
|
|
292
|
+
className={cn(
|
|
293
|
+
'text-sm leading-relaxed',
|
|
294
|
+
isUser
|
|
295
|
+
? 'rounded-xl px-4 py-3 bg-muted text-foreground'
|
|
296
|
+
: 'text-foreground'
|
|
297
|
+
)}
|
|
298
|
+
>
|
|
299
|
+
{isUser ? (
|
|
300
|
+
<>
|
|
301
|
+
{imageParts.length > 0 && (
|
|
302
|
+
<div className="mb-2 flex flex-wrap gap-2">
|
|
303
|
+
{imageParts.map((part, i) => (
|
|
304
|
+
<img
|
|
305
|
+
key={i}
|
|
306
|
+
src={part.url}
|
|
307
|
+
alt="attachment"
|
|
308
|
+
className="max-h-64 max-w-full rounded-lg object-contain"
|
|
309
|
+
/>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{otherFileParts.length > 0 && (
|
|
314
|
+
<div className="mb-2 flex flex-wrap gap-2">
|
|
315
|
+
{otherFileParts.map((part, i) => (
|
|
316
|
+
<div
|
|
317
|
+
key={i}
|
|
318
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs bg-foreground/10"
|
|
319
|
+
>
|
|
320
|
+
<FileTextIcon size={12} />
|
|
321
|
+
<span className="max-w-[150px] truncate">
|
|
322
|
+
{part.name || part.mediaType || 'file'}
|
|
323
|
+
</span>
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
{text ? (
|
|
329
|
+
<div className="whitespace-pre-wrap break-words">{text}</div>
|
|
330
|
+
) : null}
|
|
331
|
+
</>
|
|
332
|
+
) : (
|
|
333
|
+
<>
|
|
334
|
+
{message.parts?.length > 0 ? (
|
|
335
|
+
message.parts.map((part, i) => {
|
|
336
|
+
if (part.type === 'text') {
|
|
337
|
+
return <Streamdown key={i} mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{part.text}</Streamdown>;
|
|
338
|
+
}
|
|
339
|
+
if (part.type === 'file') {
|
|
340
|
+
if (part.mediaType?.startsWith('image/')) {
|
|
341
|
+
return (
|
|
342
|
+
<div key={i} className="mb-2">
|
|
343
|
+
<img src={part.url} alt="attachment" className="max-h-64 max-w-full rounded-lg object-contain" />
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return (
|
|
348
|
+
<div key={i} className="mb-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs bg-foreground/10">
|
|
349
|
+
<FileTextIcon size={12} />
|
|
350
|
+
<span className="max-w-[150px] truncate">{part.name || part.mediaType || 'file'}</span>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (part.type?.startsWith('tool-')) {
|
|
355
|
+
return <ToolCall key={part.toolCallId || i} part={part} />;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
})
|
|
359
|
+
) : text ? (
|
|
360
|
+
<Streamdown mode={isLoading ? 'streaming' : 'static'} linkSafety={linkSafety}>{text}</Streamdown>
|
|
361
|
+
) : isLoading && !hasToolParts ? (
|
|
362
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
363
|
+
<SpinnerIcon size={14} />
|
|
364
|
+
<span>Working...</span>
|
|
365
|
+
</div>
|
|
366
|
+
) : null}
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Action toolbar */}
|
|
372
|
+
{!isLoading && text && (
|
|
373
|
+
<div
|
|
374
|
+
className={cn(
|
|
375
|
+
'flex gap-1 mt-1 opacity-0 transition-opacity group-hover:opacity-100',
|
|
376
|
+
isUser ? 'justify-end' : 'justify-start'
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
<button
|
|
380
|
+
onClick={handleCopy}
|
|
381
|
+
className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
382
|
+
aria-label="Copy message"
|
|
383
|
+
>
|
|
384
|
+
{copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
|
|
385
|
+
</button>
|
|
386
|
+
{onRetry && (
|
|
387
|
+
<button
|
|
388
|
+
onClick={() => onRetry(message)}
|
|
389
|
+
className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
390
|
+
aria-label="Retry"
|
|
391
|
+
>
|
|
392
|
+
<RefreshIcon size={14} />
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
395
|
+
{isUser && onEdit && (
|
|
396
|
+
<button
|
|
397
|
+
onClick={handleEditStart}
|
|
398
|
+
className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
399
|
+
aria-label="Edit message"
|
|
400
|
+
>
|
|
401
|
+
<SquarePenIcon size={14} />
|
|
402
|
+
</button>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function ThinkingMessage() {
|
|
414
|
+
return (
|
|
415
|
+
<div className="flex gap-4 w-full justify-start">
|
|
416
|
+
<div className="flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground">
|
|
417
|
+
<SpinnerIcon size={14} />
|
|
418
|
+
<span>Thinking...</span>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useEffect, useState } from "react";
|
|
4
|
+
import { PreviewMessage, ThinkingMessage } from "./message.js";
|
|
5
|
+
import { Greeting } from "./greeting.js";
|
|
6
|
+
import { ArrowDown } from "lucide-react";
|
|
7
|
+
function Messages({ messages, status, onRetry, onEdit }) {
|
|
8
|
+
const containerRef = useRef(null);
|
|
9
|
+
const endRef = useRef(null);
|
|
10
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (isAtBottom && endRef.current) {
|
|
13
|
+
endRef.current.scrollIntoView({ behavior: "smooth" });
|
|
14
|
+
}
|
|
15
|
+
}, [messages, status, isAtBottom]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const container = containerRef.current;
|
|
18
|
+
if (!container) return;
|
|
19
|
+
const handleScroll = () => {
|
|
20
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
21
|
+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
|
|
22
|
+
};
|
|
23
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
24
|
+
return () => container.removeEventListener("scroll", handleScroll);
|
|
25
|
+
}, []);
|
|
26
|
+
const scrollToBottom = () => {
|
|
27
|
+
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
28
|
+
};
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
className: "absolute inset-0 touch-pan-y overflow-y-auto",
|
|
34
|
+
ref: containerRef,
|
|
35
|
+
children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-4 py-4 md:gap-6 md:px-6", children: [
|
|
36
|
+
messages.length === 0 && /* @__PURE__ */ jsx(Greeting, {}),
|
|
37
|
+
messages.map((message, index) => /* @__PURE__ */ jsx(
|
|
38
|
+
PreviewMessage,
|
|
39
|
+
{
|
|
40
|
+
message,
|
|
41
|
+
isLoading: status === "streaming" && index === messages.length - 1,
|
|
42
|
+
onRetry,
|
|
43
|
+
onEdit
|
|
44
|
+
},
|
|
45
|
+
message.id
|
|
46
|
+
)),
|
|
47
|
+
status === "submitted" && /* @__PURE__ */ jsx(ThinkingMessage, {}),
|
|
48
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-[24px] shrink-0", ref: endRef })
|
|
49
|
+
] })
|
|
50
|
+
}
|
|
51
|
+
),
|
|
52
|
+
!isAtBottom && /* @__PURE__ */ jsx(
|
|
53
|
+
"button",
|
|
54
|
+
{
|
|
55
|
+
className: "absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted",
|
|
56
|
+
onClick: scrollToBottom,
|
|
57
|
+
"aria-label": "Scroll to bottom",
|
|
58
|
+
children: /* @__PURE__ */ jsx(ArrowDown, { className: "size-4" })
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
] });
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
Messages
|
|
65
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { PreviewMessage, ThinkingMessage } from './message.js';
|
|
5
|
+
import { Greeting } from './greeting.js';
|
|
6
|
+
import { ArrowDown } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export function Messages({ messages, status, onRetry, onEdit }) {
|
|
9
|
+
const containerRef = useRef(null);
|
|
10
|
+
const endRef = useRef(null);
|
|
11
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
12
|
+
|
|
13
|
+
// Auto-scroll to bottom when new messages arrive
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isAtBottom && endRef.current) {
|
|
16
|
+
endRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
17
|
+
}
|
|
18
|
+
}, [messages, status, isAtBottom]);
|
|
19
|
+
|
|
20
|
+
// Track scroll position
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const container = containerRef.current;
|
|
23
|
+
if (!container) return;
|
|
24
|
+
|
|
25
|
+
const handleScroll = () => {
|
|
26
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
27
|
+
setIsAtBottom(scrollHeight - scrollTop - clientHeight < 40);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
31
|
+
return () => container.removeEventListener('scroll', handleScroll);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const scrollToBottom = () => {
|
|
35
|
+
endRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative flex-1">
|
|
40
|
+
<div
|
|
41
|
+
className="absolute inset-0 touch-pan-y overflow-y-auto"
|
|
42
|
+
ref={containerRef}
|
|
43
|
+
>
|
|
44
|
+
<div className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-4 py-4 md:gap-6 md:px-6">
|
|
45
|
+
{messages.length === 0 && <Greeting />}
|
|
46
|
+
|
|
47
|
+
{messages.map((message, index) => (
|
|
48
|
+
<PreviewMessage
|
|
49
|
+
key={message.id}
|
|
50
|
+
message={message}
|
|
51
|
+
isLoading={status === 'streaming' && index === messages.length - 1}
|
|
52
|
+
onRetry={onRetry}
|
|
53
|
+
onEdit={onEdit}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
|
|
57
|
+
{status === 'submitted' && <ThinkingMessage />}
|
|
58
|
+
|
|
59
|
+
<div className="min-h-[24px] shrink-0" ref={endRef} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{!isAtBottom && (
|
|
64
|
+
<button
|
|
65
|
+
className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background p-2 shadow-lg hover:bg-muted"
|
|
66
|
+
onClick={scrollToBottom}
|
|
67
|
+
aria-label="Scroll to bottom"
|
|
68
|
+
>
|
|
69
|
+
<ArrowDown className="size-4" />
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Streamdown } from "streamdown";
|
|
5
|
+
import { PageLayout } from "./page-layout.js";
|
|
6
|
+
import { BellIcon } from "./icons.js";
|
|
7
|
+
import { linkSafety } from "./message.js";
|
|
8
|
+
import { getNotifications, markNotificationsRead } from "../actions.js";
|
|
9
|
+
function timeAgo(ts) {
|
|
10
|
+
const seconds = Math.floor((Date.now() - ts) / 1e3);
|
|
11
|
+
if (seconds < 60) return "just now";
|
|
12
|
+
const minutes = Math.floor(seconds / 60);
|
|
13
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
14
|
+
const hours = Math.floor(minutes / 60);
|
|
15
|
+
if (hours < 24) return `${hours}h ago`;
|
|
16
|
+
const days = Math.floor(hours / 24);
|
|
17
|
+
if (days < 30) return `${days}d ago`;
|
|
18
|
+
const months = Math.floor(days / 30);
|
|
19
|
+
return `${months}mo ago`;
|
|
20
|
+
}
|
|
21
|
+
function NotificationsPage({ session }) {
|
|
22
|
+
const [notifications, setNotifications] = useState([]);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
async function load() {
|
|
26
|
+
try {
|
|
27
|
+
const result = await getNotifications();
|
|
28
|
+
setNotifications(result);
|
|
29
|
+
await markNotificationsRead();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error("Failed to load notifications:", err);
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
load();
|
|
37
|
+
}, []);
|
|
38
|
+
return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between mb-6", children: /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Notifications" }) }),
|
|
40
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
|
|
41
|
+
notifications.length,
|
|
42
|
+
" ",
|
|
43
|
+
notifications.length === 1 ? "notification" : "notifications"
|
|
44
|
+
] }),
|
|
45
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) }) : notifications.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: "No notifications yet." }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: notifications.map((n) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 p-4 border border-border rounded-lg", children: [
|
|
46
|
+
/* @__PURE__ */ jsx("div", { className: "mt-0.5 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(BellIcon, { size: 16 }) }),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
48
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm prose-sm", children: /* @__PURE__ */ jsx(Streamdown, { mode: "static", linkSafety, children: n.notification }) }),
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(n.createdAt) })
|
|
50
|
+
] })
|
|
51
|
+
] }, n.id)) })
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
NotificationsPage
|
|
56
|
+
};
|