clawport-ui 0.7.0 → 0.8.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/.env.example +4 -0
- package/README.md +2 -2
- package/app/api/chat/[id]/route.ts +2 -1
- package/app/api/conversations/[agentId]/route.ts +77 -0
- package/app/api/conversations/route.ts +11 -0
- package/app/api/kanban/chat/[id]/route.ts +2 -1
- package/app/api/onboarded/route.ts +24 -0
- package/app/api/transcribe/route.ts +2 -1
- package/app/api/tts/route.ts +2 -1
- package/app/chat/page.tsx +63 -3
- package/app/settings/page.tsx +33 -0
- package/bin/clawport.mjs +9 -4
- package/components/OnboardingWizard.tsx +13 -3
- package/components/chat/ConversationView.tsx +3 -2
- package/components/costs/CostsPage.tsx +148 -214
- package/components/costs/OptimizationPanel.tsx +179 -41
- package/components/docs/ArchitectureSection.tsx +3 -3
- package/components/docs/GettingStartedSection.tsx +3 -2
- package/components/docs/TroubleshootingSection.tsx +4 -2
- package/lib/conversation-store.test.ts +318 -0
- package/lib/conversation-store.ts +163 -0
- package/lib/conversations.ts +71 -0
- package/lib/costs.test.ts +15 -13
- package/lib/costs.ts +73 -51
- package/lib/env.ts +10 -0
- package/lib/sanitize.ts +30 -6
- package/lib/setup-detection.ts +17 -0
- package/package.json +1 -1
- package/scripts/setup.mjs +55 -10
package/.env.example
CHANGED
|
@@ -29,6 +29,10 @@ OPENCLAW_GATEWAY_TOKEN=your-gateway-token-here
|
|
|
29
29
|
# Optional
|
|
30
30
|
# ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
+
# OpenClaw gateway port (default: 18789).
|
|
33
|
+
# Change this if you configured a custom port in openclaw.json (gateway.http.port).
|
|
34
|
+
# OPENCLAW_GATEWAY_PORT=18789
|
|
35
|
+
|
|
32
36
|
# ElevenLabs API key — enables voice indicators on agent profiles.
|
|
33
37
|
# Get one at: https://elevenlabs.io
|
|
34
38
|
# Leave blank or remove this line if you don't need voice features.
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ After onboarding, verify the gateway is running:
|
|
|
42
42
|
openclaw gateway status
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
You should see your gateway URL (`localhost:18789`) and auth token. See the [OpenClaw docs](https://docs.openclaw.ai/getting-started) for more detail.
|
|
45
|
+
You should see your gateway URL (default `localhost:18789`) and auth token. If you use a custom port, `clawport setup` will detect it automatically. See the [OpenClaw docs](https://docs.openclaw.ai/getting-started) for more detail.
|
|
46
46
|
|
|
47
47
|
### 2. Install ClawPort
|
|
48
48
|
|
|
@@ -100,7 +100,7 @@ npm run dev
|
|
|
100
100
|
ClawPort reads your OpenClaw workspace to discover agents, then connects to the gateway for all AI operations:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
|
-
Browser --> ClawPort (Next.js) --> OpenClaw Gateway (localhost:18789) --> Claude
|
|
103
|
+
Browser --> ClawPort (Next.js) --> OpenClaw Gateway (localhost:18789 default) --> Claude
|
|
104
104
|
| |
|
|
105
105
|
| Text: /v1/chat/completions (streaming SSE)
|
|
106
106
|
| Vision: openclaw gateway call chat.send (CLI)
|
|
@@ -4,10 +4,11 @@ import { getAgent } from '@/lib/agents'
|
|
|
4
4
|
import { validateChatMessages } from '@/lib/validation'
|
|
5
5
|
import { hasImageContent, extractImageAttachments, buildTextPrompt, sendViaOpenClaw } from '@/lib/anthropic'
|
|
6
6
|
import OpenAI from 'openai'
|
|
7
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
7
8
|
|
|
8
9
|
// Route through the OpenClaw gateway — no separate API key needed
|
|
9
10
|
const openai = new OpenAI({
|
|
10
|
-
baseURL:
|
|
11
|
+
baseURL: gatewayBaseUrl(),
|
|
11
12
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
12
13
|
})
|
|
13
14
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { getMessages, appendMessages, clearConversation, validateAgentId, StoredMessage } from '@/lib/conversation-store'
|
|
3
|
+
import { apiErrorResponse } from '@/lib/api-error'
|
|
4
|
+
|
|
5
|
+
function isValidMessage(m: unknown): m is StoredMessage {
|
|
6
|
+
if (!m || typeof m !== 'object') return false
|
|
7
|
+
const msg = m as Record<string, unknown>
|
|
8
|
+
return (
|
|
9
|
+
typeof msg.id === 'string' && msg.id.length > 0 &&
|
|
10
|
+
(msg.role === 'user' || msg.role === 'assistant') &&
|
|
11
|
+
typeof msg.content === 'string' &&
|
|
12
|
+
(typeof msg.timestamp === 'number' || msg.timestamp === undefined)
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET(
|
|
17
|
+
_req: NextRequest,
|
|
18
|
+
{ params }: { params: Promise<{ agentId: string }> },
|
|
19
|
+
) {
|
|
20
|
+
try {
|
|
21
|
+
const { agentId } = await params
|
|
22
|
+
validateAgentId(agentId)
|
|
23
|
+
const messages = getMessages(agentId)
|
|
24
|
+
return Response.json(messages)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
|
|
27
|
+
return Response.json({ error: err.message }, { status: 400 })
|
|
28
|
+
}
|
|
29
|
+
return apiErrorResponse(err, 'Failed to load conversation')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function POST(
|
|
34
|
+
req: NextRequest,
|
|
35
|
+
{ params }: { params: Promise<{ agentId: string }> },
|
|
36
|
+
) {
|
|
37
|
+
try {
|
|
38
|
+
const { agentId } = await params
|
|
39
|
+
validateAgentId(agentId)
|
|
40
|
+
|
|
41
|
+
const body = await req.json()
|
|
42
|
+
const messages: unknown[] = body.messages
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
45
|
+
return Response.json({ error: 'messages array required' }, { status: 400 })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!messages.every(isValidMessage)) {
|
|
49
|
+
return Response.json({ error: 'Invalid message format: each message needs id, role (user|assistant), and content' }, { status: 400 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
appendMessages(agentId, messages)
|
|
53
|
+
return Response.json({ ok: true })
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
|
|
56
|
+
return Response.json({ error: err.message }, { status: 400 })
|
|
57
|
+
}
|
|
58
|
+
return apiErrorResponse(err, 'Failed to save conversation')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function DELETE(
|
|
63
|
+
_req: NextRequest,
|
|
64
|
+
{ params }: { params: Promise<{ agentId: string }> },
|
|
65
|
+
) {
|
|
66
|
+
try {
|
|
67
|
+
const { agentId } = await params
|
|
68
|
+
validateAgentId(agentId)
|
|
69
|
+
clearConversation(agentId)
|
|
70
|
+
return Response.json({ ok: true })
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof Error && err.message.startsWith('Invalid agent ID')) {
|
|
73
|
+
return Response.json({ error: err.message }, { status: 400 })
|
|
74
|
+
}
|
|
75
|
+
return apiErrorResponse(err, 'Failed to clear conversation')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { listAgentIds } from '@/lib/conversation-store'
|
|
2
|
+
import { apiErrorResponse } from '@/lib/api-error'
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const ids = listAgentIds()
|
|
7
|
+
return Response.json(ids)
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return apiErrorResponse(err, 'Failed to list conversations')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -2,9 +2,10 @@ export const runtime = 'nodejs'
|
|
|
2
2
|
|
|
3
3
|
import { getAgent } from '@/lib/agents'
|
|
4
4
|
import OpenAI from 'openai'
|
|
5
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
5
6
|
|
|
6
7
|
const openai = new OpenAI({
|
|
7
|
-
baseURL:
|
|
8
|
+
baseURL: gatewayBaseUrl(),
|
|
8
9
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
9
10
|
})
|
|
10
11
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server'
|
|
2
|
+
import { isOnboarded, setOnboarded } from '@/lib/conversation-store'
|
|
3
|
+
import { apiErrorResponse } from '@/lib/api-error'
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
return Response.json({ onboarded: isOnboarded() })
|
|
8
|
+
} catch (err) {
|
|
9
|
+
return apiErrorResponse(err, 'Failed to check onboarded status')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json()
|
|
16
|
+
if (typeof body.onboarded !== 'boolean') {
|
|
17
|
+
return Response.json({ error: 'onboarded boolean required' }, { status: 400 })
|
|
18
|
+
}
|
|
19
|
+
setOnboarded(body.onboarded)
|
|
20
|
+
return Response.json({ ok: true })
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return apiErrorResponse(err, 'Failed to update onboarded status')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const runtime = 'nodejs'
|
|
2
2
|
|
|
3
3
|
import OpenAI from 'openai'
|
|
4
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
4
5
|
|
|
5
6
|
const openai = new OpenAI({
|
|
6
|
-
baseURL:
|
|
7
|
+
baseURL: gatewayBaseUrl(),
|
|
7
8
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
8
9
|
})
|
|
9
10
|
|
package/app/api/tts/route.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const runtime = 'nodejs'
|
|
2
2
|
|
|
3
3
|
import OpenAI from 'openai'
|
|
4
|
+
import { gatewayBaseUrl } from '@/lib/env'
|
|
4
5
|
|
|
5
6
|
const openai = new OpenAI({
|
|
6
|
-
baseURL:
|
|
7
|
+
baseURL: gatewayBaseUrl(),
|
|
7
8
|
apiKey: process.env.OPENCLAW_GATEWAY_TOKEN,
|
|
8
9
|
})
|
|
9
10
|
|
package/app/chat/page.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import { useEffect, useState, useCallback, Suspense } from 'react'
|
|
2
|
+
import { useEffect, useState, useCallback, useRef, Suspense } from 'react'
|
|
3
3
|
import { useSearchParams, useRouter } from 'next/navigation'
|
|
4
4
|
import type { Agent } from '@/lib/types'
|
|
5
5
|
import { AgentList, AgentListMobile } from '@/components/chat/AgentList'
|
|
6
6
|
import { ConversationView } from '@/components/chat/ConversationView'
|
|
7
7
|
import {
|
|
8
8
|
loadConversations, saveConversations, getOrCreateConversation,
|
|
9
|
-
markRead, type ConversationStore
|
|
9
|
+
markRead, type ConversationStore, type Message,
|
|
10
|
+
fetchConversation, syncToServer, fromStoredMessage,
|
|
10
11
|
} from '@/lib/conversations'
|
|
11
12
|
|
|
12
13
|
function MessengerApp() {
|
|
@@ -31,13 +32,72 @@ function MessengerApp() {
|
|
|
31
32
|
setConversations(loadConversations())
|
|
32
33
|
}, [])
|
|
33
34
|
|
|
34
|
-
// Save conversations whenever they change
|
|
35
|
+
// Save conversations whenever they change (localStorage + server sync)
|
|
36
|
+
const prevConversationsRef = useRef<ConversationStore>({})
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
if (Object.keys(conversations).length > 0) {
|
|
37
39
|
saveConversations(conversations)
|
|
40
|
+
|
|
41
|
+
// Sync only new messages to server (fire-and-forget)
|
|
42
|
+
const prev = prevConversationsRef.current
|
|
43
|
+
for (const agentId of Object.keys(conversations)) {
|
|
44
|
+
const prevMsgs = prev[agentId]?.messages || []
|
|
45
|
+
const currMsgs = conversations[agentId]?.messages || []
|
|
46
|
+
if (currMsgs.length > prevMsgs.length) {
|
|
47
|
+
const prevIds = new Set(prevMsgs.map((m: Message) => m.id))
|
|
48
|
+
const newMsgs = currMsgs.filter((m: Message) => !prevIds.has(m.id))
|
|
49
|
+
if (newMsgs.length > 0) {
|
|
50
|
+
syncToServer(agentId, newMsgs)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
prevConversationsRef.current = conversations
|
|
38
55
|
}
|
|
39
56
|
}, [conversations])
|
|
40
57
|
|
|
58
|
+
// Background merge: fetch server conversations and merge with localStorage
|
|
59
|
+
const mergedRef = useRef(false)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (loading || agents.length === 0 || mergedRef.current) return
|
|
62
|
+
mergedRef.current = true
|
|
63
|
+
|
|
64
|
+
Promise.all(
|
|
65
|
+
agents.map(async (agent) => {
|
|
66
|
+
const serverMsgs = await fetchConversation(agent.id)
|
|
67
|
+
return { agentId: agent.id, messages: serverMsgs }
|
|
68
|
+
})
|
|
69
|
+
).then(results => {
|
|
70
|
+
setConversations(prev => {
|
|
71
|
+
let merged = { ...prev }
|
|
72
|
+
for (const { agentId, messages: serverMsgs } of results) {
|
|
73
|
+
if (serverMsgs.length === 0) continue
|
|
74
|
+
const existing = merged[agentId]
|
|
75
|
+
if (!existing) {
|
|
76
|
+
// Server has messages but localStorage doesn't — create conversation
|
|
77
|
+
merged[agentId] = {
|
|
78
|
+
agentId,
|
|
79
|
+
messages: serverMsgs.map(fromStoredMessage),
|
|
80
|
+
unread: 0,
|
|
81
|
+
lastActivity: serverMsgs[serverMsgs.length - 1].timestamp,
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
// Merge by message ID, sort by timestamp
|
|
85
|
+
const existingIds = new Set(existing.messages.map((m: Message) => m.id))
|
|
86
|
+
const newFromServer = serverMsgs
|
|
87
|
+
.filter(m => !existingIds.has(m.id))
|
|
88
|
+
.map(fromStoredMessage)
|
|
89
|
+
if (newFromServer.length > 0) {
|
|
90
|
+
const allMessages = [...existing.messages, ...newFromServer]
|
|
91
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
92
|
+
merged[agentId] = { ...existing, messages: allMessages }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return merged
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}, [loading, agents])
|
|
100
|
+
|
|
41
101
|
// Set default active agent on desktop only (don't auto-select on mobile)
|
|
42
102
|
useEffect(() => {
|
|
43
103
|
if (!loading && agents.length > 0 && !activeAgentId) {
|
package/app/settings/page.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import type { Agent } from '@/lib/types'
|
|
|
6
6
|
import { useSettings } from '@/app/settings-provider'
|
|
7
7
|
import { AgentAvatar } from '@/components/AgentAvatar'
|
|
8
8
|
import { OnboardingWizard } from '@/components/OnboardingWizard'
|
|
9
|
+
import { deleteOnServer } from '@/lib/conversations'
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Accent color presets
|
|
@@ -921,6 +922,38 @@ export default function SettingsPage() {
|
|
|
921
922
|
<Trash2 size={16} />
|
|
922
923
|
Reset All Settings
|
|
923
924
|
</button>
|
|
925
|
+
<button
|
|
926
|
+
onClick={async () => {
|
|
927
|
+
if (!window.confirm('Delete all server-side conversation data?')) return
|
|
928
|
+
try {
|
|
929
|
+
const res = await fetch('/api/conversations')
|
|
930
|
+
if (!res.ok) throw new Error()
|
|
931
|
+
const ids: string[] = await res.json()
|
|
932
|
+
ids.forEach(id => deleteOnServer(id))
|
|
933
|
+
alert('Cleared')
|
|
934
|
+
} catch {
|
|
935
|
+
alert('Failed to clear server data')
|
|
936
|
+
}
|
|
937
|
+
}}
|
|
938
|
+
className="btn-scale"
|
|
939
|
+
style={{
|
|
940
|
+
padding: 'var(--space-2) var(--space-6)',
|
|
941
|
+
borderRadius: 'var(--radius-md)',
|
|
942
|
+
background: 'var(--system-red)',
|
|
943
|
+
color: '#fff',
|
|
944
|
+
border: 'none',
|
|
945
|
+
cursor: 'pointer',
|
|
946
|
+
fontSize: 'var(--text-body)',
|
|
947
|
+
fontWeight: 'var(--weight-semibold)',
|
|
948
|
+
transition: 'all 150ms var(--ease-spring)',
|
|
949
|
+
display: 'inline-flex',
|
|
950
|
+
alignItems: 'center',
|
|
951
|
+
gap: 'var(--space-2)',
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
<Trash2 size={16} />
|
|
955
|
+
Clear Server Data
|
|
956
|
+
</button>
|
|
924
957
|
</div>
|
|
925
958
|
</section>
|
|
926
959
|
|
package/bin/clawport.mjs
CHANGED
|
@@ -84,9 +84,13 @@ function run(cmd, args = []) {
|
|
|
84
84
|
child.on('close', (code) => process.exit(code ?? 0))
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function getGatewayPort() {
|
|
88
|
+
return parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10)
|
|
89
|
+
}
|
|
90
|
+
|
|
87
91
|
async function checkGateway() {
|
|
88
92
|
try {
|
|
89
|
-
const res = await fetch(
|
|
93
|
+
const res = await fetch(`http://127.0.0.1:${getGatewayPort()}/`, {
|
|
90
94
|
signal: AbortSignal.timeout(3000),
|
|
91
95
|
})
|
|
92
96
|
return res.ok || res.status > 0
|
|
@@ -187,10 +191,11 @@ async function cmdStatus() {
|
|
|
187
191
|
// Check gateway
|
|
188
192
|
const gatewayUp = await checkGateway()
|
|
189
193
|
|
|
194
|
+
const gwPort = getGatewayPort()
|
|
190
195
|
if (gatewayUp) {
|
|
191
|
-
console.log(` ${green('+')} Gateway reachable at ${dim(
|
|
196
|
+
console.log(` ${green('+')} Gateway reachable at ${dim(`localhost:${gwPort}`)}`)
|
|
192
197
|
} else {
|
|
193
|
-
console.log(` ${red('x')} Gateway not responding at ${dim(
|
|
198
|
+
console.log(` ${red('x')} Gateway not responding at ${dim(`localhost:${gwPort}`)}`)
|
|
194
199
|
console.log(` ${dim('Start it with: openclaw gateway run')}`)
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -254,7 +259,7 @@ async function cmdDoctor() {
|
|
|
254
259
|
|
|
255
260
|
// 4. Gateway reachable
|
|
256
261
|
const gatewayUp = await checkGateway()
|
|
257
|
-
check(gatewayUp,
|
|
262
|
+
check(gatewayUp, `Gateway reachable at localhost:${getGatewayPort()}`, 'Start it with: openclaw gateway run')
|
|
258
263
|
|
|
259
264
|
// 5. Configuration -- .env.local with required vars (package root or ~/.config/clawport-ui)
|
|
260
265
|
const envPath = getEnvLocalPath()
|
|
@@ -6,6 +6,7 @@ import { useSettings } from '@/app/settings-provider'
|
|
|
6
6
|
import { useTheme } from '@/app/providers'
|
|
7
7
|
import { THEMES } from '@/lib/themes'
|
|
8
8
|
import type { ThemeId } from '@/lib/themes'
|
|
9
|
+
import { fetchOnboarded, syncOnboarded } from '@/lib/conversations'
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Accent color presets (same as settings page)
|
|
@@ -105,8 +106,16 @@ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps)
|
|
|
105
106
|
setVisible(true)
|
|
106
107
|
return
|
|
107
108
|
}
|
|
108
|
-
if (typeof window !== 'undefined'
|
|
109
|
-
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
if (localStorage.getItem('clawport-onboarded')) return
|
|
111
|
+
// Check server-side flag before showing wizard
|
|
112
|
+
fetchOnboarded().then(onboarded => {
|
|
113
|
+
if (onboarded) {
|
|
114
|
+
localStorage.setItem('clawport-onboarded', '1')
|
|
115
|
+
} else {
|
|
116
|
+
setVisible(true)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
110
119
|
}
|
|
111
120
|
}, [forceOpen]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
112
121
|
|
|
@@ -181,6 +190,7 @@ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps)
|
|
|
181
190
|
} else {
|
|
182
191
|
if (!forceOpen) {
|
|
183
192
|
localStorage.setItem('clawport-onboarded', '1')
|
|
193
|
+
syncOnboarded(true)
|
|
184
194
|
}
|
|
185
195
|
setVisible(false)
|
|
186
196
|
onClose?.()
|
|
@@ -442,7 +452,7 @@ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps)
|
|
|
442
452
|
</div>
|
|
443
453
|
{cronsStatus === 'ok' && (
|
|
444
454
|
<div style={{ fontSize: 'var(--text-caption1)', color: 'var(--text-tertiary)' }}>
|
|
445
|
-
Connected
|
|
455
|
+
Connected to gateway
|
|
446
456
|
</div>
|
|
447
457
|
)}
|
|
448
458
|
{cronsError && (
|
|
@@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'
|
|
|
3
3
|
import { useRouter } from 'next/navigation'
|
|
4
4
|
import type { Agent } from '@/lib/types'
|
|
5
5
|
import type { Conversation, ConversationStore, Message, MediaAttachment } from '@/lib/conversations'
|
|
6
|
-
import { parseMedia, addMessage, updateLastMessage } from '@/lib/conversations'
|
|
6
|
+
import { parseMedia, addMessage, updateLastMessage, deleteOnServer } from '@/lib/conversations'
|
|
7
7
|
import { buildApiContent } from '@/lib/multimodal'
|
|
8
8
|
import { generateId } from '@/lib/id'
|
|
9
9
|
import { useSettings } from '@/app/settings-provider'
|
|
@@ -633,6 +633,7 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
|
|
633
633
|
), [])
|
|
634
634
|
|
|
635
635
|
function clearChat() {
|
|
636
|
+
deleteOnServer(agent.id)
|
|
636
637
|
onUpdate(agent.id, prev => ({
|
|
637
638
|
...prev,
|
|
638
639
|
[agent.id]: {
|
|
@@ -724,7 +725,7 @@ export function ConversationView({ agent, conversation, onUpdate, onBack }: Conv
|
|
|
724
725
|
textOverflow: 'ellipsis',
|
|
725
726
|
whiteSpace: 'nowrap',
|
|
726
727
|
}}>
|
|
727
|
-
{agent.title}
|
|
728
|
+
{agent.title}{messages.length > 1 && ' · Synced'}
|
|
728
729
|
</div>
|
|
729
730
|
</div>
|
|
730
731
|
</div>
|