@decido/kernel-bridge 1.0.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/.turbo/turbo-build.log +13 -0
- package/.turbo/turbo-lint.log +30 -0
- package/package.json +37 -0
- package/src/ai/components/PeerNetworkPanel.tsx +219 -0
- package/src/ai/components/TokenWalletPanel.tsx +172 -0
- package/src/ai/hooks/usePeerMesh.ts +79 -0
- package/src/ai/hooks/useTokenWallet.ts +35 -0
- package/src/ai/index.ts +96 -0
- package/src/ai/services/EmbeddingService.ts +119 -0
- package/src/ai/services/InferenceRouter.ts +347 -0
- package/src/ai/services/LocalAgentResponder.ts +199 -0
- package/src/ai/services/MLXBridge.ts +278 -0
- package/src/ai/services/OllamaService.ts +326 -0
- package/src/ai/services/PeerMesh.ts +373 -0
- package/src/ai/services/TokenWallet.ts +237 -0
- package/src/ai/services/providers/AnthropicProvider.ts +229 -0
- package/src/ai/services/providers/GeminiProvider.ts +121 -0
- package/src/ai/services/providers/LLMProvider.ts +72 -0
- package/src/ai/services/providers/OllamaProvider.ts +84 -0
- package/src/ai/services/providers/OpenAIProvider.ts +178 -0
- package/src/crypto.ts +54 -0
- package/src/index.ts +4 -0
- package/src/kernel.ts +376 -0
- package/src/rehydration.ts +52 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
Debugger listening on ws://127.0.0.1:58975/0314506c-e1de-4a58-be5e-b4ac774586f4
|
|
3
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
4
|
+
Debugger attached.
|
|
5
|
+
|
|
6
|
+
> @decido/kernel-bridge@1.0.0 build /Users/julioramirez/dev/active/OnBoardingDecido/packages/kernel-bridge
|
|
7
|
+
> tsc
|
|
8
|
+
|
|
9
|
+
Debugger listening on ws://127.0.0.1:58979/db35dc6b-ec5f-499c-b2bb-a281886a7590
|
|
10
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
11
|
+
Debugger attached.
|
|
12
|
+
Waiting for the debugger to disconnect...
|
|
13
|
+
Waiting for the debugger to disconnect...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
Debugger listening on ws://127.0.0.1:60130/80df8f38-b3d4-4cfb-a50d-56cde2cc32c8
|
|
3
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
4
|
+
Debugger attached.
|
|
5
|
+
|
|
6
|
+
> @macia/kernel-bridge@1.0.0 lint /Users/julioramirez/dev/active/Decido/packages/core-decido/kernel-bridge
|
|
7
|
+
> eslint "src/**/*.ts*"
|
|
8
|
+
|
|
9
|
+
Debugger listening on ws://127.0.0.1:60166/f14ee9d9-5a8a-4ad3-a957-a0f39414225c
|
|
10
|
+
For help, see: https://nodejs.org/en/docs/inspector
|
|
11
|
+
Debugger attached.
|
|
12
|
+
|
|
13
|
+
Oops! Something went wrong! :(
|
|
14
|
+
|
|
15
|
+
ESLint: 9.39.3
|
|
16
|
+
|
|
17
|
+
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
|
|
18
|
+
|
|
19
|
+
From ESLint v9.0.0, the default configuration file is now eslint.config.js.
|
|
20
|
+
If you are using a .eslintrc.* file, please follow the migration guide
|
|
21
|
+
to update your configuration file to the new format:
|
|
22
|
+
|
|
23
|
+
https://eslint.org/docs/latest/use/configure/migration-guide
|
|
24
|
+
|
|
25
|
+
If you still have problems after following the migration guide, please stop by
|
|
26
|
+
https://eslint.org/chat/help to chat with the team.
|
|
27
|
+
|
|
28
|
+
Waiting for the debugger to disconnect...
|
|
29
|
+
[41m[30mβELIFECYCLEβ[39m[49m [31mCommand failed with exit code 2.[39m
|
|
30
|
+
Waiting for the debugger to disconnect...
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decido/kernel-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tauri v2 IPC Bridge for Decido OS",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
15
|
+
"@google/genai": "^1.42.0",
|
|
16
|
+
"@tauri-apps/api": "^2.0.0-rc.0",
|
|
17
|
+
"@tauri-apps/plugin-haptics": "^2.0.0-rc.0",
|
|
18
|
+
"@tauri-apps/plugin-notification": "^2.0.0-rc.0",
|
|
19
|
+
"openai": "^6.24.0",
|
|
20
|
+
"socket.io-client": "^4.8.3",
|
|
21
|
+
"uuid": "^13.0.0",
|
|
22
|
+
"@decido/sdk": "1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"@types/uuid": "^11.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"license": "UNLICENSED",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"lint": "eslint \"src/**/*.ts*\"",
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts --minify --clean"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerNetworkPanel β P2P mesh control panel
|
|
3
|
+
*
|
|
4
|
+
* Shows connected peers, hosting controls, and shared results.
|
|
5
|
+
* Uses Decido Aura design tokens for full theme compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback } from 'react';
|
|
9
|
+
import { usePeerMesh } from '../hooks/usePeerMesh';
|
|
10
|
+
import type { PeerInfo } from '../services/PeerMesh';
|
|
11
|
+
|
|
12
|
+
// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
13
|
+
|
|
14
|
+
function timeAgo(ts: number): string {
|
|
15
|
+
const diff = Date.now() - ts;
|
|
16
|
+
if (diff < 60_000) return 'just now';
|
|
17
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
18
|
+
return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// βββ Component ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
22
|
+
|
|
23
|
+
export function PeerNetworkPanel() {
|
|
24
|
+
const {
|
|
25
|
+
peers, isHosting, isConnected, sharedResults, peerCount,
|
|
26
|
+
startHost, stopHost, connectToPeer, disconnect,
|
|
27
|
+
} = usePeerMesh();
|
|
28
|
+
|
|
29
|
+
const [peerAddress, setPeerAddress] = useState('');
|
|
30
|
+
const [hostPort, setHostPort] = useState('9876');
|
|
31
|
+
|
|
32
|
+
const handleConnect = useCallback(() => {
|
|
33
|
+
if (!peerAddress.trim()) return;
|
|
34
|
+
connectToPeer(peerAddress.trim());
|
|
35
|
+
setPeerAddress('');
|
|
36
|
+
}, [peerAddress, connectToPeer]);
|
|
37
|
+
|
|
38
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
39
|
+
if (e.key === 'Enter') handleConnect();
|
|
40
|
+
}, [handleConnect]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col gap-3 p-4 bg-surface-secondary rounded-2xl border border-border-subtle">
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<div className="flex items-center justify-between">
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<span className="text-lg">π</span>
|
|
48
|
+
<h3 className="text-sm font-semibold text-text-primary">Peer Network</h3>
|
|
49
|
+
{peerCount > 0 && (
|
|
50
|
+
<span className="px-1.5 py-0.5 text-[10px] font-bold rounded-full bg-emerald-500/20 text-accent-green">
|
|
51
|
+
{peerCount}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
<StatusBadge isHosting={isHosting} isConnected={isConnected} />
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Connection Controls */}
|
|
59
|
+
<div className="flex flex-col gap-2 p-3 rounded-xl bg-surface-tertiary/50">
|
|
60
|
+
{/* Host Toggle */}
|
|
61
|
+
<div className="flex items-center justify-between">
|
|
62
|
+
<div className="flex flex-col">
|
|
63
|
+
<span className="text-[11px] font-medium text-text-secondary">Host a Group</span>
|
|
64
|
+
<span className="text-[9px] text-text-muted">Others can connect to you</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<input
|
|
68
|
+
className="w-16 px-2 py-1 text-[11px] font-mono text-text-primary bg-surface-primary
|
|
69
|
+
border border-border-subtle rounded-lg text-center
|
|
70
|
+
focus:border-border-strong focus:outline-none transition-colors"
|
|
71
|
+
value={hostPort}
|
|
72
|
+
onChange={(e) => setHostPort(e.target.value)}
|
|
73
|
+
placeholder="Port"
|
|
74
|
+
disabled={isHosting}
|
|
75
|
+
/>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => isHosting ? stopHost() : startHost(Number(hostPort))}
|
|
78
|
+
className={`px-3 py-1 text-[11px] font-medium rounded-lg transition-all
|
|
79
|
+
${isHosting
|
|
80
|
+
? 'bg-red-500/15 text-accent-red hover:bg-red-500/25'
|
|
81
|
+
: 'bg-emerald-500/15 text-accent-green hover:bg-emerald-500/25'
|
|
82
|
+
}`}
|
|
83
|
+
>
|
|
84
|
+
{isHosting ? 'βΉ Stop' : 'βΆ Start'}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Divider */}
|
|
90
|
+
<div className="h-px bg-border-subtle" />
|
|
91
|
+
|
|
92
|
+
{/* Connect to Peer */}
|
|
93
|
+
<div className="flex flex-col gap-1.5">
|
|
94
|
+
<span className="text-[11px] font-medium text-text-secondary">Join a Group</span>
|
|
95
|
+
<div className="flex gap-2">
|
|
96
|
+
<input
|
|
97
|
+
className="flex-1 px-3 py-1.5 text-[11px] font-mono text-text-primary bg-surface-primary
|
|
98
|
+
border border-border-subtle rounded-lg placeholder:text-text-muted
|
|
99
|
+
focus:border-border-strong focus:outline-none transition-colors"
|
|
100
|
+
value={peerAddress}
|
|
101
|
+
onChange={(e) => setPeerAddress(e.target.value)}
|
|
102
|
+
onKeyDown={handleKeyDown}
|
|
103
|
+
placeholder="192.168.1.10:9876"
|
|
104
|
+
disabled={isConnected}
|
|
105
|
+
/>
|
|
106
|
+
{isConnected ? (
|
|
107
|
+
<button
|
|
108
|
+
onClick={disconnect}
|
|
109
|
+
className="px-3 py-1.5 text-[11px] font-medium rounded-lg bg-red-500/15 text-accent-red
|
|
110
|
+
hover:bg-red-500/25 transition-all"
|
|
111
|
+
>
|
|
112
|
+
Disconnect
|
|
113
|
+
</button>
|
|
114
|
+
) : (
|
|
115
|
+
<button
|
|
116
|
+
onClick={handleConnect}
|
|
117
|
+
className="px-3 py-1.5 text-[11px] font-medium rounded-lg bg-cyan-500/15 text-accent-cyan
|
|
118
|
+
hover:bg-cyan-500/25 transition-all"
|
|
119
|
+
>
|
|
120
|
+
Connect
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Connected Peers */}
|
|
128
|
+
{peers.length > 0 && (
|
|
129
|
+
<div className="flex flex-col gap-1.5">
|
|
130
|
+
<span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">
|
|
131
|
+
Connected Peers
|
|
132
|
+
</span>
|
|
133
|
+
{peers.map((peer) => (
|
|
134
|
+
<PeerRow key={peer.id} peer={peer} />
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Empty State */}
|
|
140
|
+
{!isHosting && !isConnected && peers.length === 0 && (
|
|
141
|
+
<div className="flex flex-col items-center justify-center py-5 text-text-muted gap-1.5">
|
|
142
|
+
<span className="text-2xl opacity-40">π</span>
|
|
143
|
+
<span className="text-xs text-center">
|
|
144
|
+
Host a group or connect to a peer<br />
|
|
145
|
+
to share AI results
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Shared Results */}
|
|
151
|
+
{sharedResults.length > 0 && (
|
|
152
|
+
<div className="flex flex-col gap-1.5 mt-1">
|
|
153
|
+
<span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">
|
|
154
|
+
Shared Results ({sharedResults.length})
|
|
155
|
+
</span>
|
|
156
|
+
<div className="flex flex-col gap-1 max-h-40 overflow-y-auto">
|
|
157
|
+
{sharedResults.slice(-8).reverse().map((r, i) => (
|
|
158
|
+
<div
|
|
159
|
+
key={`${r.timestamp}-${i}`}
|
|
160
|
+
className="px-3 py-2 rounded-xl bg-surface-glass text-[10px]"
|
|
161
|
+
>
|
|
162
|
+
<div className="flex items-center justify-between mb-1">
|
|
163
|
+
<span className="font-medium text-accent-purple">
|
|
164
|
+
{r.peerId.slice(0, 8)}
|
|
165
|
+
</span>
|
|
166
|
+
<span className="text-text-muted font-mono">{r.model}</span>
|
|
167
|
+
</div>
|
|
168
|
+
<p className="text-text-secondary line-clamp-2">{r.response.slice(0, 120)}...</p>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// βββ Subcomponents ββββββββββββββββββββββββββββββββββββββββββ
|
|
179
|
+
|
|
180
|
+
function StatusBadge({ isHosting, isConnected }: { isHosting: boolean; isConnected: boolean }) {
|
|
181
|
+
if (isHosting) {
|
|
182
|
+
return (
|
|
183
|
+
<span className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-emerald-500/15 text-[10px] font-medium text-accent-green">
|
|
184
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
|
185
|
+
Hosting
|
|
186
|
+
</span>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (isConnected) {
|
|
190
|
+
return (
|
|
191
|
+
<span className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-cyan-500/15 text-[10px] font-medium text-accent-cyan">
|
|
192
|
+
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
|
193
|
+
Connected
|
|
194
|
+
</span>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return (
|
|
198
|
+
<span className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-surface-glass text-[10px] text-text-muted">
|
|
199
|
+
<span className="w-1.5 h-1.5 rounded-full bg-surface-tertiary" />
|
|
200
|
+
Offline
|
|
201
|
+
</span>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function PeerRow({ peer }: { peer: PeerInfo }) {
|
|
206
|
+
return (
|
|
207
|
+
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-surface-glass transition-colors hover:bg-surface-tertiary/50">
|
|
208
|
+
<div className="flex items-center gap-2">
|
|
209
|
+
<span className="w-2 h-2 rounded-full bg-emerald-400" />
|
|
210
|
+
<span className="text-xs font-medium text-text-primary">{peer.name}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex items-center gap-2 text-[10px] text-text-muted">
|
|
213
|
+
<span className="font-mono">{peer.address}</span>
|
|
214
|
+
<span>Β·</span>
|
|
215
|
+
<span>{timeAgo(peer.lastSeen)}</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenWalletPanel β Visual spending dashboard for AI providers
|
|
3
|
+
*
|
|
4
|
+
* Shows total tokens, estimated cost, and per-provider breakdown.
|
|
5
|
+
* Uses Decido Aura design tokens for full theme compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { useTokenWallet } from '../hooks/useTokenWallet';
|
|
10
|
+
import type { TokenUsageEntry } from '../services/TokenWallet';
|
|
11
|
+
|
|
12
|
+
// βββ Provider colors (accent palette) βββββββββββββββββββββββ
|
|
13
|
+
|
|
14
|
+
const PROVIDER_COLORS: Record<string, string> = {
|
|
15
|
+
ollama: 'text-accent-green',
|
|
16
|
+
gemini: 'text-accent-blue',
|
|
17
|
+
anthropic: 'text-accent-purple',
|
|
18
|
+
openai: 'text-accent-cyan',
|
|
19
|
+
mlx: 'text-accent-amber',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const PROVIDER_BG: Record<string, string> = {
|
|
23
|
+
ollama: 'bg-emerald-500/10',
|
|
24
|
+
gemini: 'bg-blue-500/10',
|
|
25
|
+
anthropic: 'bg-purple-500/10',
|
|
26
|
+
openai: 'bg-cyan-500/10',
|
|
27
|
+
mlx: 'bg-amber-500/10',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PROVIDER_ICONS: Record<string, string> = {
|
|
31
|
+
ollama: 'π¦',
|
|
32
|
+
gemini: 'β¨',
|
|
33
|
+
anthropic: 'π§ ',
|
|
34
|
+
openai: 'β‘',
|
|
35
|
+
mlx: 'π',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
39
|
+
|
|
40
|
+
function formatCost(usd: number): string {
|
|
41
|
+
if (usd === 0) return 'FREE';
|
|
42
|
+
if (usd < 0.01) return `$${usd.toFixed(6)}`;
|
|
43
|
+
if (usd < 1) return `$${usd.toFixed(4)}`;
|
|
44
|
+
return `$${usd.toFixed(2)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatTokens(n: number): string {
|
|
48
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
49
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
50
|
+
return String(n);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function timeAgo(ts: number): string {
|
|
54
|
+
const diff = Date.now() - ts;
|
|
55
|
+
if (diff < 60_000) return 'just now';
|
|
56
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
57
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
58
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// βββ Component ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
62
|
+
|
|
63
|
+
export function TokenWalletPanel() {
|
|
64
|
+
const { summary, totalTokens, totalCost, totalCalls, byProvider, clearHistory, getRecentHistory } = useTokenWallet();
|
|
65
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
66
|
+
|
|
67
|
+
const providers = Object.entries(byProvider).sort((a, b) => b[1].cost - a[1].cost);
|
|
68
|
+
const history = showHistory ? getRecentHistory(15) : [];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-3 p-4 bg-surface-secondary rounded-2xl border border-border-subtle">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div className="flex items-center justify-between">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span className="text-lg">π°</span>
|
|
76
|
+
<h3 className="text-sm font-semibold text-text-primary">Token Wallet</h3>
|
|
77
|
+
</div>
|
|
78
|
+
<button
|
|
79
|
+
onClick={clearHistory}
|
|
80
|
+
className="text-[10px] px-2 py-0.5 rounded-md bg-surface-glass text-text-muted
|
|
81
|
+
hover:bg-surface-tertiary hover:text-text-secondary transition-colors"
|
|
82
|
+
>
|
|
83
|
+
Clear
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Total Stats */}
|
|
88
|
+
<div className="grid grid-cols-3 gap-2">
|
|
89
|
+
<StatCard label="Tokens" value={formatTokens(totalTokens)} accent="text-accent-cyan" />
|
|
90
|
+
<StatCard label="Cost" value={formatCost(totalCost)} accent={totalCost === 0 ? 'text-accent-green' : 'text-accent-amber'} />
|
|
91
|
+
<StatCard label="Calls" value={String(totalCalls)} accent="text-accent-blue" />
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Per-Provider Breakdown */}
|
|
95
|
+
{providers.length > 0 && (
|
|
96
|
+
<div className="flex flex-col gap-1.5 mt-1">
|
|
97
|
+
<span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">By Provider</span>
|
|
98
|
+
{providers.map(([id, data]) => (
|
|
99
|
+
<div
|
|
100
|
+
key={id}
|
|
101
|
+
className={`flex items-center justify-between px-3 py-2 rounded-xl ${PROVIDER_BG[id] || 'bg-surface-glass'} transition-colors`}
|
|
102
|
+
>
|
|
103
|
+
<div className="flex items-center gap-2">
|
|
104
|
+
<span className="text-sm">{PROVIDER_ICONS[id] || 'π€'}</span>
|
|
105
|
+
<span className={`text-xs font-medium ${PROVIDER_COLORS[id] || 'text-text-primary'}`}>
|
|
106
|
+
{id.charAt(0).toUpperCase() + id.slice(1)}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="flex items-center gap-3 text-[11px]">
|
|
110
|
+
<span className="text-text-muted">{data.calls} calls</span>
|
|
111
|
+
<span className="text-text-secondary font-mono">{formatTokens(data.tokens)}</span>
|
|
112
|
+
<span className="font-semibold text-text-primary font-mono">{formatCost(data.cost)}</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Empty State */}
|
|
120
|
+
{providers.length === 0 && (
|
|
121
|
+
<div className="flex flex-col items-center justify-center py-6 text-text-muted gap-1">
|
|
122
|
+
<span className="text-2xl opacity-50">π</span>
|
|
123
|
+
<span className="text-xs">No AI calls recorded yet</span>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{/* History Toggle */}
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => setShowHistory(!showHistory)}
|
|
130
|
+
className="text-[10px] text-text-muted hover:text-text-secondary transition-colors self-center"
|
|
131
|
+
>
|
|
132
|
+
{showHistory ? 'β² Hide recent' : 'βΌ Show recent calls'}
|
|
133
|
+
</button>
|
|
134
|
+
|
|
135
|
+
{/* History List */}
|
|
136
|
+
{showHistory && history.length > 0 && (
|
|
137
|
+
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
|
138
|
+
{history.reverse().map((entry, i) => (
|
|
139
|
+
<HistoryRow key={`${entry.timestamp}-${i}`} entry={entry} />
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// βββ Subcomponents ββββββββββββββββββββββββββββββββββββββββββ
|
|
148
|
+
|
|
149
|
+
function StatCard({ label, value, accent }: { label: string; value: string; accent: string }) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex flex-col items-center p-2 rounded-xl bg-surface-tertiary/50">
|
|
152
|
+
<span className="text-[9px] text-text-muted uppercase tracking-wider">{label}</span>
|
|
153
|
+
<span className={`text-base font-bold font-mono ${accent}`}>{value}</span>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function HistoryRow({ entry }: { entry: TokenUsageEntry }) {
|
|
159
|
+
return (
|
|
160
|
+
<div className="flex items-center justify-between px-2 py-1.5 rounded-lg bg-surface-glass text-[10px]">
|
|
161
|
+
<div className="flex items-center gap-1.5">
|
|
162
|
+
<span>{PROVIDER_ICONS[entry.provider] || 'π€'}</span>
|
|
163
|
+
<span className="text-text-secondary font-mono">{entry.model.split('/').pop()?.slice(0, 18)}</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<span className="text-text-muted">{formatTokens(entry.totalTokens)}</span>
|
|
167
|
+
<span className="text-text-primary font-mono">{formatCost(entry.estimatedCostUsd)}</span>
|
|
168
|
+
<span className="text-text-muted">{timeAgo(entry.timestamp)}</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePeerMesh β React hook for P2P group mesh
|
|
3
|
+
*
|
|
4
|
+
* Exposes peer list, hosting controls, and context sharing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
peerMesh,
|
|
10
|
+
type PeerInfo,
|
|
11
|
+
type SharedResult,
|
|
12
|
+
} from '../services/PeerMesh';
|
|
13
|
+
|
|
14
|
+
export function usePeerMesh() {
|
|
15
|
+
const [peers, setPeers] = useState<PeerInfo[]>([]);
|
|
16
|
+
const [isHosting, setIsHosting] = useState(false);
|
|
17
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
18
|
+
const [sharedResults, setSharedResults] = useState<SharedResult[]>([]);
|
|
19
|
+
const resultsRef = useRef(sharedResults);
|
|
20
|
+
resultsRef.current = sharedResults;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const unsubPeers = peerMesh.onPeersChanged((newPeers) => {
|
|
24
|
+
setPeers([...newPeers]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const unsubHosting = peerMesh.onHostingChanged((hosting) => {
|
|
28
|
+
setIsHosting(hosting);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const unsubConnected = peerMesh.onConnectionChanged((connected) => {
|
|
32
|
+
setIsConnected(connected);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const unsubResult = peerMesh.onSharedResult((result) => {
|
|
36
|
+
setSharedResults(prev => [...prev.slice(-49), result]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
unsubPeers();
|
|
41
|
+
unsubHosting();
|
|
42
|
+
unsubConnected();
|
|
43
|
+
unsubResult();
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const startHost = useCallback((port?: number) => {
|
|
48
|
+
peerMesh.startHost(port);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const stopHost = useCallback(() => {
|
|
52
|
+
peerMesh.stopHost();
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const connectToPeer = useCallback((address: string) => {
|
|
56
|
+
peerMesh.connectToPeer(address);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const disconnect = useCallback(() => {
|
|
60
|
+
peerMesh.disconnect();
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const shareResult = useCallback((result: Omit<SharedResult, 'peerId' | 'timestamp'>) => {
|
|
64
|
+
peerMesh.shareResult(result);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
peers,
|
|
69
|
+
isHosting,
|
|
70
|
+
isConnected,
|
|
71
|
+
sharedResults,
|
|
72
|
+
peerCount: peers.length,
|
|
73
|
+
startHost,
|
|
74
|
+
stopHost,
|
|
75
|
+
connectToPeer,
|
|
76
|
+
disconnect,
|
|
77
|
+
shareResult,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTokenWallet β React hook for the Token Wallet
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to wallet changes and exposes a reactive summary.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
8
|
+
import { tokenWallet, type WalletSummary, type TokenUsageEntry } from '../services/TokenWallet';
|
|
9
|
+
|
|
10
|
+
export function useTokenWallet() {
|
|
11
|
+
const [summary, setSummary] = useState<WalletSummary>(tokenWallet.getSummary());
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const unsubscribe = tokenWallet.subscribe(setSummary);
|
|
15
|
+
return unsubscribe;
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const clearHistory = useCallback(() => {
|
|
19
|
+
tokenWallet.clearHistory();
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const getRecentHistory = useCallback((count = 20): TokenUsageEntry[] => {
|
|
23
|
+
return tokenWallet.getRecentHistory(count);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
summary,
|
|
28
|
+
clearHistory,
|
|
29
|
+
getRecentHistory,
|
|
30
|
+
totalTokens: summary.totalTokens,
|
|
31
|
+
totalCost: summary.totalCostUsd,
|
|
32
|
+
totalCalls: summary.totalCalls,
|
|
33
|
+
byProvider: summary.byProvider,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/ai/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// βββ AI Services ββββββββββββββββββββββββββββββββββββββββββββ
|
|
2
|
+
|
|
3
|
+
// Inference Router
|
|
4
|
+
export {
|
|
5
|
+
initProviders,
|
|
6
|
+
loadProviderKeys,
|
|
7
|
+
saveProviderKeys,
|
|
8
|
+
setProviderApiKey,
|
|
9
|
+
getProviderStatuses,
|
|
10
|
+
getAllProviders,
|
|
11
|
+
routeChat,
|
|
12
|
+
routeInference,
|
|
13
|
+
inferenceRouter,
|
|
14
|
+
type InferenceBackend,
|
|
15
|
+
type TaskType,
|
|
16
|
+
type RouterConfig,
|
|
17
|
+
type RoutedResponse,
|
|
18
|
+
} from './services/InferenceRouter';
|
|
19
|
+
|
|
20
|
+
// LLM Providers
|
|
21
|
+
export type {
|
|
22
|
+
LLMProvider,
|
|
23
|
+
ChatMessage,
|
|
24
|
+
ChatOptions,
|
|
25
|
+
ChatResult,
|
|
26
|
+
ProviderStatus,
|
|
27
|
+
} from './services/providers/LLMProvider';
|
|
28
|
+
|
|
29
|
+
// Ollama Service
|
|
30
|
+
export {
|
|
31
|
+
chat as ollamaChat,
|
|
32
|
+
chat,
|
|
33
|
+
chatStream as ollamaChatStream,
|
|
34
|
+
chatStream,
|
|
35
|
+
isOllamaAvailable,
|
|
36
|
+
listModels as ollamaListModels,
|
|
37
|
+
clearConversationHistory,
|
|
38
|
+
parseToolCalls,
|
|
39
|
+
stripToolCalls,
|
|
40
|
+
type ToolCallRequest,
|
|
41
|
+
type ChatWithToolsResult,
|
|
42
|
+
} from './services/OllamaService';
|
|
43
|
+
|
|
44
|
+
// MLX Bridge
|
|
45
|
+
export {
|
|
46
|
+
listAvailableModels as mlxListModels,
|
|
47
|
+
runInference as mlxRunInference,
|
|
48
|
+
compareModels as mlxCompareModels,
|
|
49
|
+
pullModel as mlxPullModel,
|
|
50
|
+
benchmarkModel as mlxBenchmarkModel,
|
|
51
|
+
startLoRATraining,
|
|
52
|
+
type MLXModelInfo,
|
|
53
|
+
type InferenceResult,
|
|
54
|
+
type TrainingProgress,
|
|
55
|
+
type BenchmarkResult,
|
|
56
|
+
} from './services/MLXBridge';
|
|
57
|
+
|
|
58
|
+
// Embedding Service
|
|
59
|
+
export { embeddingService } from './services/EmbeddingService';
|
|
60
|
+
|
|
61
|
+
// Local Agent Responder
|
|
62
|
+
export { processLocalMessage } from './services/LocalAgentResponder';
|
|
63
|
+
|
|
64
|
+
// LLM Providers (individual implementations)
|
|
65
|
+
export { AnthropicProvider } from './services/providers/AnthropicProvider';
|
|
66
|
+
export { OpenAIProvider } from './services/providers/OpenAIProvider';
|
|
67
|
+
export { GeminiProvider } from './services/providers/GeminiProvider';
|
|
68
|
+
export { OllamaProvider } from './services/providers/OllamaProvider';
|
|
69
|
+
|
|
70
|
+
// βββ Token Wallet βββββββββββββββββββββββββββββββββββββββββββ
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
tokenWallet,
|
|
74
|
+
type TokenUsageEntry,
|
|
75
|
+
type WalletSummary,
|
|
76
|
+
type ProviderSummary,
|
|
77
|
+
} from './services/TokenWallet';
|
|
78
|
+
|
|
79
|
+
// βββ P2P Peer Mesh ββββββββββββββββββββββββββββββββββββββββββ
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
peerMesh,
|
|
83
|
+
type PeerInfo,
|
|
84
|
+
type SharedResult,
|
|
85
|
+
} from './services/PeerMesh';
|
|
86
|
+
|
|
87
|
+
// βββ React Hooks ββββββββββββββββββββββββββββββββββββββββββββ
|
|
88
|
+
|
|
89
|
+
export { useTokenWallet } from './hooks/useTokenWallet';
|
|
90
|
+
export { usePeerMesh } from './hooks/usePeerMesh';
|
|
91
|
+
|
|
92
|
+
// βββ UI Components ββββββββββββββββββββββββββββββββββββββββββ
|
|
93
|
+
|
|
94
|
+
export { TokenWalletPanel } from './components/TokenWalletPanel';
|
|
95
|
+
export { PeerNetworkPanel } from './components/PeerNetworkPanel';
|
|
96
|
+
|