@199-bio/engram 0.4.1 → 0.4.3
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/dist/index.js +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/web/chat-handler.d.ts.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/storage/database.ts +12 -0
- package/src/web/chat-handler.ts +472 -0
- package/src/web/server.ts +39 -1
- package/src/web/static/app.js +143 -0
- package/src/web/static/index.html +30 -0
- package/src/web/static/style.css +210 -0
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-handler.d.ts","sourceRoot":"","sources":["../../src/web/chat-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAkB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiLtD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,mBAAmB,CAAgC;gBAE/C,OAAO,EAAE;QACnB,EAAE,EAAE,cAAc,CAAC;QACnB,KAAK,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE,YAAY,CAAC;KACtB;IAWD,YAAY,IAAI,OAAO;IAIvB,YAAY,IAAI,IAAI;IAId,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAgFlC,WAAW;CAgL1B"}
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAY/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;YAwMT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@199-bio/engram",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Give Claude a perfect memory. Local-first MCP server with hybrid search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"node": ">=18.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
50
51
|
"better-sqlite3": "^11.10.0",
|
|
51
52
|
"chrono-node": "^2.7.0",
|
package/src/index.ts
CHANGED
package/src/storage/database.ts
CHANGED
|
@@ -471,6 +471,18 @@ export class EngramDatabase {
|
|
|
471
471
|
return result.changes > 0;
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
updateEntity(id: string, updates: { name?: string; type?: Entity["type"] }): Entity | null {
|
|
475
|
+
const entity = this.getEntity(id);
|
|
476
|
+
if (!entity) return null;
|
|
477
|
+
|
|
478
|
+
const newName = updates.name ?? entity.name;
|
|
479
|
+
const newType = updates.type ?? entity.type;
|
|
480
|
+
|
|
481
|
+
const stmt = this.db.prepare("UPDATE entities SET name = ?, type = ? WHERE id = ?");
|
|
482
|
+
stmt.run(newName, newType, id);
|
|
483
|
+
return this.getEntity(id);
|
|
484
|
+
}
|
|
485
|
+
|
|
474
486
|
// ============ Observation Operations ============
|
|
475
487
|
|
|
476
488
|
addObservation(
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Handler for Engram Web Interface
|
|
3
|
+
* Uses Claude Haiku 4.5 with tools for entity/memory management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
7
|
+
import { EngramDatabase, Entity, Memory } from "../storage/database.js";
|
|
8
|
+
import { KnowledgeGraph } from "../graph/knowledge-graph.js";
|
|
9
|
+
import { HybridSearch } from "../retrieval/hybrid.js";
|
|
10
|
+
|
|
11
|
+
// Tool definitions for Claude
|
|
12
|
+
const TOOLS: Anthropic.Tool[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "list_entities",
|
|
15
|
+
description: "List all entities in the knowledge graph. Use this to see what people, organizations, and places are stored.",
|
|
16
|
+
input_schema: {
|
|
17
|
+
type: "object" as const,
|
|
18
|
+
properties: {
|
|
19
|
+
type: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["person", "organization", "place"],
|
|
22
|
+
description: "Filter by entity type (optional)",
|
|
23
|
+
},
|
|
24
|
+
limit: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Maximum number of entities to return (default: 50)",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: [],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "get_entity",
|
|
34
|
+
description: "Get detailed information about an entity including its observations and relationships.",
|
|
35
|
+
input_schema: {
|
|
36
|
+
type: "object" as const,
|
|
37
|
+
properties: {
|
|
38
|
+
name: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "The entity name to look up",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["name"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "delete_entity",
|
|
48
|
+
description: "Delete an entity and all its observations and relationships. Use this to remove incorrect or duplicate entities.",
|
|
49
|
+
input_schema: {
|
|
50
|
+
type: "object" as const,
|
|
51
|
+
properties: {
|
|
52
|
+
name: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "The entity name to delete",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ["name"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "merge_entities",
|
|
62
|
+
description: "Merge one entity into another. All observations and relationships from the source will be moved to the target, then the source is deleted. Use this to fix duplicates.",
|
|
63
|
+
input_schema: {
|
|
64
|
+
type: "object" as const,
|
|
65
|
+
properties: {
|
|
66
|
+
keep: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "The entity name to keep (target)",
|
|
69
|
+
},
|
|
70
|
+
merge: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "The entity name to merge and delete (source)",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ["keep", "merge"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "rename_entity",
|
|
80
|
+
description: "Rename an entity to a new name.",
|
|
81
|
+
input_schema: {
|
|
82
|
+
type: "object" as const,
|
|
83
|
+
properties: {
|
|
84
|
+
old_name: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Current entity name",
|
|
87
|
+
},
|
|
88
|
+
new_name: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "New entity name",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: ["old_name", "new_name"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "delete_relationship",
|
|
98
|
+
description: "Delete a relationship between two entities.",
|
|
99
|
+
input_schema: {
|
|
100
|
+
type: "object" as const,
|
|
101
|
+
properties: {
|
|
102
|
+
from: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Source entity name",
|
|
105
|
+
},
|
|
106
|
+
to: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Target entity name",
|
|
109
|
+
},
|
|
110
|
+
type: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Relationship type (e.g., 'works_at', 'knows')",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
required: ["from", "to", "type"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "search_memories",
|
|
120
|
+
description: "Search through stored memories.",
|
|
121
|
+
input_schema: {
|
|
122
|
+
type: "object" as const,
|
|
123
|
+
properties: {
|
|
124
|
+
query: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Search query",
|
|
127
|
+
},
|
|
128
|
+
limit: {
|
|
129
|
+
type: "number",
|
|
130
|
+
description: "Maximum results (default: 10)",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ["query"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "delete_memory",
|
|
138
|
+
description: "Delete a memory by its ID.",
|
|
139
|
+
input_schema: {
|
|
140
|
+
type: "object" as const,
|
|
141
|
+
properties: {
|
|
142
|
+
id: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "The memory ID to delete",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ["id"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "find_duplicates",
|
|
152
|
+
description: "Find potential duplicate entities that could be merged.",
|
|
153
|
+
input_schema: {
|
|
154
|
+
type: "object" as const,
|
|
155
|
+
properties: {},
|
|
156
|
+
required: [],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "auto_tidy",
|
|
161
|
+
description: "Automatically merge all detected duplicate entities.",
|
|
162
|
+
input_schema: {
|
|
163
|
+
type: "object" as const,
|
|
164
|
+
properties: {},
|
|
165
|
+
required: [],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const SYSTEM_PROMPT = `You are a helpful assistant for managing Engram, a personal memory system. You help users:
|
|
171
|
+
- View and search their memories
|
|
172
|
+
- Manage entities (people, organizations, places)
|
|
173
|
+
- Fix incorrect relationships
|
|
174
|
+
- Merge duplicate entities
|
|
175
|
+
- Delete incorrect data
|
|
176
|
+
|
|
177
|
+
Be concise and helpful. When making changes, confirm what you did. If asked to do something destructive, confirm first unless the user is explicit.
|
|
178
|
+
|
|
179
|
+
When listing entities or memories, format them clearly. Use the tools available to you.`;
|
|
180
|
+
|
|
181
|
+
interface ChatMessage {
|
|
182
|
+
role: "user" | "assistant";
|
|
183
|
+
content: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class ChatHandler {
|
|
187
|
+
private client: Anthropic | null = null;
|
|
188
|
+
private db: EngramDatabase;
|
|
189
|
+
private graph: KnowledgeGraph;
|
|
190
|
+
private search: HybridSearch;
|
|
191
|
+
private conversationHistory: Anthropic.MessageParam[] = [];
|
|
192
|
+
|
|
193
|
+
constructor(options: {
|
|
194
|
+
db: EngramDatabase;
|
|
195
|
+
graph: KnowledgeGraph;
|
|
196
|
+
search: HybridSearch;
|
|
197
|
+
}) {
|
|
198
|
+
this.db = options.db;
|
|
199
|
+
this.graph = options.graph;
|
|
200
|
+
this.search = options.search;
|
|
201
|
+
|
|
202
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
203
|
+
if (apiKey) {
|
|
204
|
+
this.client = new Anthropic({ apiKey });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
isConfigured(): boolean {
|
|
209
|
+
return this.client !== null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
clearHistory(): void {
|
|
213
|
+
this.conversationHistory = [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async chat(userMessage: string): Promise<string> {
|
|
217
|
+
if (!this.client) {
|
|
218
|
+
return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Add user message to history
|
|
222
|
+
this.conversationHistory.push({
|
|
223
|
+
role: "user",
|
|
224
|
+
content: userMessage,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Keep conversation history manageable (last 20 messages)
|
|
229
|
+
if (this.conversationHistory.length > 20) {
|
|
230
|
+
this.conversationHistory = this.conversationHistory.slice(-20);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let response = await this.client.messages.create({
|
|
234
|
+
model: "claude-haiku-4-5-20241022",
|
|
235
|
+
max_tokens: 1024,
|
|
236
|
+
system: SYSTEM_PROMPT,
|
|
237
|
+
tools: TOOLS,
|
|
238
|
+
messages: this.conversationHistory,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Handle tool use loop
|
|
242
|
+
while (response.stop_reason === "tool_use") {
|
|
243
|
+
const toolUseBlocks = response.content.filter(
|
|
244
|
+
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
248
|
+
for (const toolUse of toolUseBlocks) {
|
|
249
|
+
const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
|
|
250
|
+
toolResults.push({
|
|
251
|
+
type: "tool_result",
|
|
252
|
+
tool_use_id: toolUse.id,
|
|
253
|
+
content: JSON.stringify(result),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add assistant response and tool results to history
|
|
258
|
+
this.conversationHistory.push({
|
|
259
|
+
role: "assistant",
|
|
260
|
+
content: response.content,
|
|
261
|
+
});
|
|
262
|
+
this.conversationHistory.push({
|
|
263
|
+
role: "user",
|
|
264
|
+
content: toolResults,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Continue the conversation
|
|
268
|
+
response = await this.client.messages.create({
|
|
269
|
+
model: "claude-haiku-4-5-20241022",
|
|
270
|
+
max_tokens: 1024,
|
|
271
|
+
system: SYSTEM_PROMPT,
|
|
272
|
+
tools: TOOLS,
|
|
273
|
+
messages: this.conversationHistory,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extract text response
|
|
278
|
+
const textBlocks = response.content.filter(
|
|
279
|
+
(block): block is Anthropic.TextBlock => block.type === "text"
|
|
280
|
+
);
|
|
281
|
+
const assistantMessage = textBlocks.map((b) => b.text).join("\n");
|
|
282
|
+
|
|
283
|
+
// Add final response to history
|
|
284
|
+
this.conversationHistory.push({
|
|
285
|
+
role: "assistant",
|
|
286
|
+
content: response.content,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return assistantMessage;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
292
|
+
return `Error: ${message}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private async executeTool(name: string, input: Record<string, unknown>): Promise<unknown> {
|
|
297
|
+
switch (name) {
|
|
298
|
+
case "list_entities": {
|
|
299
|
+
const type = input.type as Entity["type"] | undefined;
|
|
300
|
+
const limit = (input.limit as number) || 50;
|
|
301
|
+
const entities = this.graph.listEntities(type, limit);
|
|
302
|
+
return {
|
|
303
|
+
entities: entities.map((e) => ({
|
|
304
|
+
name: e.name,
|
|
305
|
+
type: e.type,
|
|
306
|
+
id: e.id,
|
|
307
|
+
})),
|
|
308
|
+
count: entities.length,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case "get_entity": {
|
|
313
|
+
const name = input.name as string;
|
|
314
|
+
const details = this.graph.getEntityDetails(name);
|
|
315
|
+
if (!details) {
|
|
316
|
+
return { error: `Entity not found: ${name}` };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
name: details.name,
|
|
320
|
+
type: details.type,
|
|
321
|
+
observations: details.observations.map((o) => ({
|
|
322
|
+
content: o.content.substring(0, 200) + (o.content.length > 200 ? "..." : ""),
|
|
323
|
+
confidence: o.confidence,
|
|
324
|
+
})),
|
|
325
|
+
relationships_from: details.relationsFrom.map((r) => ({
|
|
326
|
+
type: r.type,
|
|
327
|
+
to: r.targetEntity.name,
|
|
328
|
+
})),
|
|
329
|
+
relationships_to: details.relationsTo.map((r) => ({
|
|
330
|
+
type: r.type,
|
|
331
|
+
from: r.sourceEntity.name,
|
|
332
|
+
})),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
case "delete_entity": {
|
|
337
|
+
const name = input.name as string;
|
|
338
|
+
const entity = this.db.findEntityByName(name);
|
|
339
|
+
if (!entity) {
|
|
340
|
+
return { error: `Entity not found: ${name}` };
|
|
341
|
+
}
|
|
342
|
+
this.db.deleteEntity(entity.id);
|
|
343
|
+
return { success: true, deleted: name };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case "merge_entities": {
|
|
347
|
+
const keepName = input.keep as string;
|
|
348
|
+
const mergeName = input.merge as string;
|
|
349
|
+
|
|
350
|
+
const keepEntity = this.db.findEntityByName(keepName);
|
|
351
|
+
const mergeEntity = this.db.findEntityByName(mergeName);
|
|
352
|
+
|
|
353
|
+
if (!keepEntity) {
|
|
354
|
+
return { error: `Entity not found: ${keepName}` };
|
|
355
|
+
}
|
|
356
|
+
if (!mergeEntity) {
|
|
357
|
+
return { error: `Entity not found: ${mergeName}` };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = this.db.mergeEntities(keepEntity.id, mergeEntity.id);
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
kept: keepName,
|
|
364
|
+
merged: mergeName,
|
|
365
|
+
observations_moved: result.observationsMoved,
|
|
366
|
+
relations_moved: result.relationsMoved,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
case "rename_entity": {
|
|
371
|
+
const oldName = input.old_name as string;
|
|
372
|
+
const newName = input.new_name as string;
|
|
373
|
+
|
|
374
|
+
const entity = this.db.findEntityByName(oldName);
|
|
375
|
+
if (!entity) {
|
|
376
|
+
return { error: `Entity not found: ${oldName}` };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.db.updateEntity(entity.id, { name: newName });
|
|
380
|
+
return { success: true, old_name: oldName, new_name: newName };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case "delete_relationship": {
|
|
384
|
+
const fromName = input.from as string;
|
|
385
|
+
const toName = input.to as string;
|
|
386
|
+
const type = input.type as string;
|
|
387
|
+
|
|
388
|
+
const fromEntity = this.db.findEntityByName(fromName);
|
|
389
|
+
const toEntity = this.db.findEntityByName(toName);
|
|
390
|
+
|
|
391
|
+
if (!fromEntity) {
|
|
392
|
+
return { error: `Entity not found: ${fromName}` };
|
|
393
|
+
}
|
|
394
|
+
if (!toEntity) {
|
|
395
|
+
return { error: `Entity not found: ${toName}` };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const relation = this.db.findRelation(fromEntity.id, toEntity.id, type);
|
|
399
|
+
if (!relation) {
|
|
400
|
+
return { error: `Relationship not found: ${fromName} -[${type}]-> ${toName}` };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.db.deleteRelation(relation.id);
|
|
404
|
+
return { success: true, deleted: `${fromName} -[${type}]-> ${toName}` };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
case "search_memories": {
|
|
408
|
+
const query = input.query as string;
|
|
409
|
+
const limit = (input.limit as number) || 10;
|
|
410
|
+
|
|
411
|
+
const results = await this.search.search(query, { limit });
|
|
412
|
+
return {
|
|
413
|
+
results: results.map((r) => ({
|
|
414
|
+
id: r.memory.id,
|
|
415
|
+
content: r.memory.content.substring(0, 300) + (r.memory.content.length > 300 ? "..." : ""),
|
|
416
|
+
timestamp: r.memory.timestamp.toISOString(),
|
|
417
|
+
score: r.score.toFixed(3),
|
|
418
|
+
})),
|
|
419
|
+
count: results.length,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case "delete_memory": {
|
|
424
|
+
const id = input.id as string;
|
|
425
|
+
const memory = this.db.getMemory(id);
|
|
426
|
+
if (!memory) {
|
|
427
|
+
return { error: `Memory not found: ${id}` };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await this.search.removeFromIndex(id);
|
|
431
|
+
this.db.deleteMemory(id);
|
|
432
|
+
return { success: true, deleted_id: id };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case "find_duplicates": {
|
|
436
|
+
const duplicates = this.db.findDuplicateEntities();
|
|
437
|
+
return {
|
|
438
|
+
groups: duplicates.map((d) => ({
|
|
439
|
+
keep: d.entity.name,
|
|
440
|
+
duplicates: d.potentialDuplicates.map((p) => p.name),
|
|
441
|
+
})),
|
|
442
|
+
total_duplicates: duplicates.reduce((sum, d) => sum + d.potentialDuplicates.length, 0),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
case "auto_tidy": {
|
|
447
|
+
const duplicates = this.db.findDuplicateEntities();
|
|
448
|
+
let totalMerged = 0;
|
|
449
|
+
let observationsMoved = 0;
|
|
450
|
+
let relationsMoved = 0;
|
|
451
|
+
|
|
452
|
+
for (const group of duplicates) {
|
|
453
|
+
for (const dupe of group.potentialDuplicates) {
|
|
454
|
+
const result = this.db.mergeEntities(group.entity.id, dupe.id);
|
|
455
|
+
totalMerged++;
|
|
456
|
+
observationsMoved += result.observationsMoved;
|
|
457
|
+
relationsMoved += result.relationsMoved;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
entities_merged: totalMerged,
|
|
463
|
+
observations_moved: observationsMoved,
|
|
464
|
+
relations_moved: relationsMoved,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
default:
|
|
469
|
+
return { error: `Unknown tool: ${name}` };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
package/src/web/server.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "url";
|
|
|
10
10
|
import { EngramDatabase } from "../storage/database.js";
|
|
11
11
|
import { KnowledgeGraph } from "../graph/knowledge-graph.js";
|
|
12
12
|
import { HybridSearch } from "../retrieval/hybrid.js";
|
|
13
|
-
import {
|
|
13
|
+
import { ChatHandler } from "./chat-handler.js";
|
|
14
14
|
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = path.dirname(__filename);
|
|
@@ -38,12 +38,18 @@ export class EngramWebServer {
|
|
|
38
38
|
private db: EngramDatabase;
|
|
39
39
|
private graph: KnowledgeGraph;
|
|
40
40
|
private search: HybridSearch;
|
|
41
|
+
private chat: ChatHandler;
|
|
41
42
|
private port: number;
|
|
42
43
|
|
|
43
44
|
constructor(options: WebServerOptions) {
|
|
44
45
|
this.db = options.db;
|
|
45
46
|
this.graph = options.graph;
|
|
46
47
|
this.search = options.search;
|
|
48
|
+
this.chat = new ChatHandler({
|
|
49
|
+
db: options.db,
|
|
50
|
+
graph: options.graph,
|
|
51
|
+
search: options.search,
|
|
52
|
+
});
|
|
47
53
|
this.port = options.port || 3847;
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -266,6 +272,38 @@ export class EngramWebServer {
|
|
|
266
272
|
return;
|
|
267
273
|
}
|
|
268
274
|
|
|
275
|
+
// GET /api/chat/status - check if chat is configured
|
|
276
|
+
if (pathname === "/api/chat/status" && method === "GET") {
|
|
277
|
+
res.end(JSON.stringify({
|
|
278
|
+
configured: this.chat.isConfigured(),
|
|
279
|
+
message: this.chat.isConfigured()
|
|
280
|
+
? "Chat is ready"
|
|
281
|
+
: "Set ANTHROPIC_API_KEY environment variable to enable chat",
|
|
282
|
+
}));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// POST /api/chat - send a message
|
|
287
|
+
if (pathname === "/api/chat" && method === "POST") {
|
|
288
|
+
const { message } = body as { message: string };
|
|
289
|
+
if (!message) {
|
|
290
|
+
res.writeHead(400);
|
|
291
|
+
res.end(JSON.stringify({ error: "Message is required" }));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const response = await this.chat.chat(message);
|
|
296
|
+
res.end(JSON.stringify({ response }));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// POST /api/chat/clear - clear chat history
|
|
301
|
+
if (pathname === "/api/chat/clear" && method === "POST") {
|
|
302
|
+
this.chat.clearHistory();
|
|
303
|
+
res.end(JSON.stringify({ success: true }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
269
307
|
// 404 for unknown API routes
|
|
270
308
|
res.writeHead(404);
|
|
271
309
|
res.end(JSON.stringify({ error: "Not found" }));
|
package/src/web/static/app.js
CHANGED
|
@@ -357,6 +357,149 @@ entityModal.addEventListener('click', (e) => {
|
|
|
357
357
|
if (e.target === entityModal) entityModal.classList.add('hidden');
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
// ============ Chat Panel ============
|
|
361
|
+
|
|
362
|
+
const chatPanel = document.getElementById('chat-panel');
|
|
363
|
+
const chatToggle = document.getElementById('chat-toggle');
|
|
364
|
+
const chatClose = document.getElementById('chat-close');
|
|
365
|
+
const chatClear = document.getElementById('chat-clear');
|
|
366
|
+
const chatMessages = document.getElementById('chat-messages');
|
|
367
|
+
const chatStatus = document.getElementById('chat-status');
|
|
368
|
+
const chatForm = document.getElementById('chat-form');
|
|
369
|
+
const chatInput = document.getElementById('chat-input');
|
|
370
|
+
|
|
371
|
+
let chatConfigured = false;
|
|
372
|
+
|
|
373
|
+
// Check if chat is configured
|
|
374
|
+
async function checkChatStatus() {
|
|
375
|
+
try {
|
|
376
|
+
const data = await api('/api/chat/status');
|
|
377
|
+
chatConfigured = data.configured;
|
|
378
|
+
if (!chatConfigured) {
|
|
379
|
+
chatStatus.textContent = 'Set ANTHROPIC_API_KEY env var to enable chat';
|
|
380
|
+
chatStatus.classList.add('error');
|
|
381
|
+
chatInput.disabled = true;
|
|
382
|
+
chatInput.placeholder = 'Chat disabled - API key not configured';
|
|
383
|
+
} else {
|
|
384
|
+
chatStatus.textContent = '';
|
|
385
|
+
chatStatus.classList.remove('error');
|
|
386
|
+
chatInput.disabled = false;
|
|
387
|
+
chatInput.placeholder = 'Ask me to manage entities...';
|
|
388
|
+
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
chatStatus.textContent = 'Failed to connect to chat service';
|
|
391
|
+
chatStatus.classList.add('error');
|
|
392
|
+
chatInput.disabled = true;
|
|
393
|
+
chatInput.placeholder = 'Chat unavailable';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Toggle chat panel
|
|
398
|
+
function toggleChat() {
|
|
399
|
+
const isHidden = chatPanel.classList.contains('hidden');
|
|
400
|
+
chatPanel.classList.toggle('hidden');
|
|
401
|
+
chatToggle.classList.toggle('active', isHidden);
|
|
402
|
+
document.body.classList.toggle('chat-open', isHidden);
|
|
403
|
+
|
|
404
|
+
if (isHidden) {
|
|
405
|
+
checkChatStatus();
|
|
406
|
+
chatInput.focus();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Add message to chat
|
|
411
|
+
function addChatMessage(content, role) {
|
|
412
|
+
const div = document.createElement('div');
|
|
413
|
+
div.className = `chat-message ${role}`;
|
|
414
|
+
|
|
415
|
+
// Simple markdown-like parsing
|
|
416
|
+
const formatted = content
|
|
417
|
+
.replace(/\n\n/g, '</p><p>')
|
|
418
|
+
.replace(/\n/g, '<br>')
|
|
419
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
420
|
+
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
421
|
+
|
|
422
|
+
div.innerHTML = `<p>${formatted}</p>`;
|
|
423
|
+
chatMessages.appendChild(div);
|
|
424
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Send chat message
|
|
428
|
+
async function sendChatMessage(message) {
|
|
429
|
+
if (!message.trim()) return;
|
|
430
|
+
|
|
431
|
+
// Add user message
|
|
432
|
+
addChatMessage(message, 'user');
|
|
433
|
+
chatInput.value = '';
|
|
434
|
+
chatInput.disabled = true;
|
|
435
|
+
|
|
436
|
+
// Show thinking indicator
|
|
437
|
+
const thinkingDiv = document.createElement('div');
|
|
438
|
+
thinkingDiv.className = 'chat-message thinking';
|
|
439
|
+
thinkingDiv.innerHTML = '<p>Thinking...</p>';
|
|
440
|
+
chatMessages.appendChild(thinkingDiv);
|
|
441
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const data = await api('/api/chat', {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
body: { message },
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Remove thinking indicator
|
|
450
|
+
thinkingDiv.remove();
|
|
451
|
+
|
|
452
|
+
// Add assistant response
|
|
453
|
+
addChatMessage(data.response, 'assistant');
|
|
454
|
+
|
|
455
|
+
// Refresh data in case something changed
|
|
456
|
+
loadStats();
|
|
457
|
+
if (currentView === 'entities') loadEntities(entityTypeFilter.value);
|
|
458
|
+
if (currentView === 'graph') loadGraph();
|
|
459
|
+
if (currentView === 'memories') loadMemories(searchInput.value);
|
|
460
|
+
|
|
461
|
+
} catch (e) {
|
|
462
|
+
thinkingDiv.remove();
|
|
463
|
+
addChatMessage('Error: Failed to get response. Please try again.', 'assistant');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
chatInput.disabled = false;
|
|
467
|
+
chatInput.focus();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Clear chat history
|
|
471
|
+
async function clearChatHistory() {
|
|
472
|
+
try {
|
|
473
|
+
await api('/api/chat/clear', { method: 'POST' });
|
|
474
|
+
// Keep only the initial welcome message
|
|
475
|
+
chatMessages.innerHTML = `
|
|
476
|
+
<div class="chat-message assistant">
|
|
477
|
+
<p>Hi! I can help you manage your memories and entities.</p>
|
|
478
|
+
<p><strong>Examples:</strong></p>
|
|
479
|
+
<ul>
|
|
480
|
+
<li>"Show me all entities"</li>
|
|
481
|
+
<li>"Find duplicates"</li>
|
|
482
|
+
<li>"Merge Boris into Boris Djordjevic"</li>
|
|
483
|
+
<li>"Delete the entity 'crashed'"</li>
|
|
484
|
+
</ul>
|
|
485
|
+
<p style="font-size: 0.8em; color: var(--text-muted); margin-top: 0.5rem;">Requires ANTHROPIC_API_KEY environment variable.</p>
|
|
486
|
+
</div>
|
|
487
|
+
`;
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.error('Failed to clear chat history', e);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Chat event listeners
|
|
494
|
+
chatToggle.addEventListener('click', toggleChat);
|
|
495
|
+
chatClose.addEventListener('click', toggleChat);
|
|
496
|
+
chatClear.addEventListener('click', clearChatHistory);
|
|
497
|
+
|
|
498
|
+
chatForm.addEventListener('submit', (e) => {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
sendChatMessage(chatInput.value);
|
|
501
|
+
});
|
|
502
|
+
|
|
360
503
|
// Initialize
|
|
361
504
|
loadStats();
|
|
362
505
|
loadMemories();
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<button class="nav-btn" data-view="graph">Graph</button>
|
|
16
16
|
</nav>
|
|
17
17
|
<div class="stats" id="stats"></div>
|
|
18
|
+
<button id="chat-toggle" class="chat-toggle" title="Open Chat Assistant">Chat</button>
|
|
18
19
|
</header>
|
|
19
20
|
|
|
20
21
|
<main>
|
|
@@ -90,6 +91,35 @@
|
|
|
90
91
|
</div>
|
|
91
92
|
</div>
|
|
92
93
|
|
|
94
|
+
<!-- Chat Panel -->
|
|
95
|
+
<aside id="chat-panel" class="chat-panel hidden">
|
|
96
|
+
<div class="chat-header">
|
|
97
|
+
<h3>Chat Assistant</h3>
|
|
98
|
+
<div class="chat-actions">
|
|
99
|
+
<button id="chat-clear" title="Clear history">Clear</button>
|
|
100
|
+
<button id="chat-close" title="Close">×</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div id="chat-messages" class="chat-messages">
|
|
104
|
+
<div class="chat-message assistant">
|
|
105
|
+
<p>Hi! I can help you manage your memories and entities.</p>
|
|
106
|
+
<p><strong>Examples:</strong></p>
|
|
107
|
+
<ul>
|
|
108
|
+
<li>"Show me all entities"</li>
|
|
109
|
+
<li>"Find duplicates"</li>
|
|
110
|
+
<li>"Merge Boris into Boris Djordjevic"</li>
|
|
111
|
+
<li>"Delete the entity 'crashed'"</li>
|
|
112
|
+
</ul>
|
|
113
|
+
<p style="font-size: 0.8em; color: var(--text-muted); margin-top: 0.5rem;">Requires ANTHROPIC_API_KEY environment variable.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div id="chat-status" class="chat-status"></div>
|
|
117
|
+
<form id="chat-form" class="chat-input-form">
|
|
118
|
+
<input type="text" id="chat-input" placeholder="Ask me to manage entities..." autocomplete="off">
|
|
119
|
+
<button type="submit">Send</button>
|
|
120
|
+
</form>
|
|
121
|
+
</aside>
|
|
122
|
+
|
|
93
123
|
<script src="app.js"></script>
|
|
94
124
|
</body>
|
|
95
125
|
</html>
|
package/src/web/static/style.css
CHANGED
|
@@ -428,6 +428,216 @@ button:disabled {
|
|
|
428
428
|
font-variant-numeric: tabular-nums;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
/* Chat toggle button */
|
|
432
|
+
.chat-toggle {
|
|
433
|
+
margin-left: 1rem;
|
|
434
|
+
background: var(--accent);
|
|
435
|
+
font-size: 0.8125rem;
|
|
436
|
+
padding: 0.5rem 1rem;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.chat-toggle:hover {
|
|
440
|
+
background: var(--accent-hover);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.chat-toggle.active {
|
|
444
|
+
background: var(--text-primary);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* Chat panel */
|
|
448
|
+
.chat-panel {
|
|
449
|
+
position: fixed;
|
|
450
|
+
top: 0;
|
|
451
|
+
right: 0;
|
|
452
|
+
width: 400px;
|
|
453
|
+
height: 100vh;
|
|
454
|
+
background: var(--bg-primary);
|
|
455
|
+
border-left: 1px solid var(--border);
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
box-shadow: -4px 0 20px var(--shadow);
|
|
459
|
+
z-index: 100;
|
|
460
|
+
transform: translateX(0);
|
|
461
|
+
transition: transform 0.2s ease;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.chat-panel.hidden {
|
|
465
|
+
transform: translateX(100%);
|
|
466
|
+
visibility: hidden;
|
|
467
|
+
pointer-events: none;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.chat-header {
|
|
471
|
+
display: flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
justify-content: space-between;
|
|
474
|
+
padding: 1rem 1.25rem;
|
|
475
|
+
background: var(--bg-secondary);
|
|
476
|
+
border-bottom: 1px solid var(--border);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.chat-header h3 {
|
|
480
|
+
font-size: 1rem;
|
|
481
|
+
font-weight: 500;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.chat-actions {
|
|
485
|
+
display: flex;
|
|
486
|
+
gap: 0.5rem;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.chat-actions button {
|
|
490
|
+
font-size: 0.75rem;
|
|
491
|
+
padding: 0.375rem 0.625rem;
|
|
492
|
+
background: var(--bg-tertiary);
|
|
493
|
+
color: var(--text-secondary);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.chat-actions button:hover {
|
|
497
|
+
background: var(--border);
|
|
498
|
+
color: var(--text-primary);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
#chat-close {
|
|
502
|
+
font-size: 1.25rem;
|
|
503
|
+
padding: 0.25rem 0.5rem;
|
|
504
|
+
line-height: 1;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.chat-messages {
|
|
508
|
+
flex: 1;
|
|
509
|
+
overflow-y: auto;
|
|
510
|
+
padding: 1rem;
|
|
511
|
+
display: flex;
|
|
512
|
+
flex-direction: column;
|
|
513
|
+
gap: 1rem;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.chat-message {
|
|
517
|
+
padding: 0.875rem 1rem;
|
|
518
|
+
font-size: 0.9375rem;
|
|
519
|
+
line-height: 1.6;
|
|
520
|
+
max-width: 90%;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.chat-message.user {
|
|
524
|
+
background: var(--accent);
|
|
525
|
+
color: white;
|
|
526
|
+
align-self: flex-end;
|
|
527
|
+
border-radius: 2px;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.chat-message.assistant {
|
|
531
|
+
background: var(--bg-secondary);
|
|
532
|
+
border: 1px solid var(--border);
|
|
533
|
+
align-self: flex-start;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.chat-message p {
|
|
537
|
+
margin: 0 0 0.5rem;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.chat-message p:last-child {
|
|
541
|
+
margin-bottom: 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.chat-message ul {
|
|
545
|
+
margin: 0.5rem 0 0;
|
|
546
|
+
padding-left: 1.25rem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.chat-message li {
|
|
550
|
+
margin: 0.25rem 0;
|
|
551
|
+
font-size: 0.875rem;
|
|
552
|
+
color: var(--text-secondary);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.chat-message.user li {
|
|
556
|
+
color: rgba(255, 255, 255, 0.85);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.chat-message pre {
|
|
560
|
+
background: var(--bg-tertiary);
|
|
561
|
+
padding: 0.5rem 0.75rem;
|
|
562
|
+
margin: 0.5rem 0;
|
|
563
|
+
overflow-x: auto;
|
|
564
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
565
|
+
font-size: 0.8125rem;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.chat-message.thinking {
|
|
569
|
+
background: transparent;
|
|
570
|
+
border: 1px dashed var(--border);
|
|
571
|
+
color: var(--text-muted);
|
|
572
|
+
font-style: italic;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.chat-status {
|
|
576
|
+
padding: 0.5rem 1rem;
|
|
577
|
+
font-size: 0.75rem;
|
|
578
|
+
color: var(--text-muted);
|
|
579
|
+
text-align: center;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.chat-status.error {
|
|
583
|
+
color: var(--danger);
|
|
584
|
+
background: rgba(220, 38, 38, 0.1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.chat-status:empty {
|
|
588
|
+
display: none;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.chat-input-form {
|
|
592
|
+
display: flex;
|
|
593
|
+
gap: 0.5rem;
|
|
594
|
+
padding: 1rem;
|
|
595
|
+
background: var(--bg-secondary);
|
|
596
|
+
border-top: 1px solid var(--border);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.chat-input-form input {
|
|
600
|
+
flex: 1;
|
|
601
|
+
font-family: inherit;
|
|
602
|
+
font-size: 0.9375rem;
|
|
603
|
+
padding: 0.625rem 0.875rem;
|
|
604
|
+
border: 1px solid var(--border);
|
|
605
|
+
background: var(--bg-primary);
|
|
606
|
+
color: var(--text-primary);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.chat-input-form input:focus {
|
|
610
|
+
outline: none;
|
|
611
|
+
border-color: var(--accent);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.chat-input-form input:disabled {
|
|
615
|
+
background: var(--bg-tertiary);
|
|
616
|
+
color: var(--text-muted);
|
|
617
|
+
cursor: not-allowed;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.chat-input-form input::placeholder {
|
|
621
|
+
color: var(--text-muted);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.chat-input-form button {
|
|
625
|
+
background: var(--accent);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.chat-input-form button:hover {
|
|
629
|
+
background: var(--accent-hover);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.chat-input-form button:disabled {
|
|
633
|
+
background: var(--text-muted);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* Adjust main content when chat is open */
|
|
637
|
+
body.chat-open main {
|
|
638
|
+
margin-right: 400px;
|
|
639
|
+
}
|
|
640
|
+
|
|
431
641
|
/* Responsive */
|
|
432
642
|
@media (max-width: 640px) {
|
|
433
643
|
header {
|