@getjack/jack 0.1.22 → 0.1.23
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/package.json +1 -1
- package/src/commands/clone.ts +5 -5
- package/src/commands/down.ts +44 -69
- package/src/commands/link.ts +9 -6
- package/src/commands/new.ts +54 -76
- package/src/commands/publish.ts +7 -2
- package/src/commands/secrets.ts +2 -1
- package/src/commands/services.ts +41 -15
- package/src/commands/update.ts +2 -2
- package/src/index.ts +43 -2
- package/src/lib/agent-integration.ts +217 -0
- package/src/lib/auth/login-flow.ts +2 -1
- package/src/lib/binding-validator.ts +2 -3
- package/src/lib/build-helper.ts +7 -1
- package/src/lib/hooks.ts +101 -21
- package/src/lib/managed-down.ts +32 -55
- package/src/lib/project-detection.ts +48 -21
- package/src/lib/project-operations.ts +31 -13
- package/src/lib/prompts.ts +16 -23
- package/src/lib/services/db-execute.ts +39 -0
- package/src/lib/services/sql-classifier.test.ts +2 -2
- package/src/lib/services/sql-classifier.ts +5 -4
- package/src/lib/version-check.ts +15 -10
- package/src/lib/zip-packager.ts +16 -0
- package/src/mcp/resources/index.ts +42 -2
- package/src/templates/index.ts +63 -3
- package/templates/ai-chat/.jack.json +29 -0
- package/templates/ai-chat/bun.lock +18 -0
- package/templates/ai-chat/package.json +14 -0
- package/templates/ai-chat/public/chat.js +149 -0
- package/templates/ai-chat/public/index.html +209 -0
- package/templates/ai-chat/src/index.ts +105 -0
- package/templates/ai-chat/wrangler.jsonc +12 -0
- package/templates/semantic-search/.jack.json +26 -0
- package/templates/semantic-search/bun.lock +18 -0
- package/templates/semantic-search/package.json +12 -0
- package/templates/semantic-search/public/app.js +120 -0
- package/templates/semantic-search/public/index.html +210 -0
- package/templates/semantic-search/schema.sql +5 -0
- package/templates/semantic-search/src/index.ts +144 -0
- package/templates/semantic-search/tsconfig.json +13 -0
- package/templates/semantic-search/wrangler.jsonc +27 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const messagesEl = document.getElementById('messages');
|
|
2
|
+
const inputEl = document.getElementById('input');
|
|
3
|
+
const sendBtn = document.getElementById('send');
|
|
4
|
+
|
|
5
|
+
let history = [];
|
|
6
|
+
let isLoading = false;
|
|
7
|
+
|
|
8
|
+
function setLoading(loading) {
|
|
9
|
+
isLoading = loading;
|
|
10
|
+
inputEl.disabled = loading;
|
|
11
|
+
sendBtn.disabled = loading;
|
|
12
|
+
sendBtn.textContent = loading ? '...' : 'Send';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clearEmptyState() {
|
|
16
|
+
const emptyState = messagesEl.querySelector('.empty-state');
|
|
17
|
+
if (emptyState) {
|
|
18
|
+
emptyState.remove();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function appendMessage(role, content, className = '') {
|
|
23
|
+
clearEmptyState();
|
|
24
|
+
const el = document.createElement('div');
|
|
25
|
+
el.className = `message ${role} ${className}`.trim();
|
|
26
|
+
el.textContent = content;
|
|
27
|
+
messagesEl.appendChild(el);
|
|
28
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
29
|
+
return el;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function sendMessage() {
|
|
33
|
+
const content = inputEl.value.trim();
|
|
34
|
+
if (!content || isLoading) return;
|
|
35
|
+
|
|
36
|
+
// Add user message to history and display
|
|
37
|
+
history.push({ role: 'user', content });
|
|
38
|
+
appendMessage('user', content);
|
|
39
|
+
inputEl.value = '';
|
|
40
|
+
|
|
41
|
+
// Create assistant message placeholder
|
|
42
|
+
const assistantEl = appendMessage('assistant', '', 'typing');
|
|
43
|
+
setLoading(true);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch('/api/chat', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ messages: history }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
let errorMessage = 'Something went wrong. Please try again.';
|
|
54
|
+
try {
|
|
55
|
+
const err = await response.json();
|
|
56
|
+
if (err.error) {
|
|
57
|
+
errorMessage = err.error;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Use default error message
|
|
61
|
+
}
|
|
62
|
+
assistantEl.textContent = errorMessage;
|
|
63
|
+
assistantEl.className = 'message assistant error';
|
|
64
|
+
setLoading(false);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stream response
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let assistantContent = '';
|
|
72
|
+
let buffer = '';
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
if (done) break;
|
|
77
|
+
|
|
78
|
+
buffer += decoder.decode(value, { stream: true });
|
|
79
|
+
|
|
80
|
+
// Process complete SSE messages
|
|
81
|
+
const lines = buffer.split('\n');
|
|
82
|
+
// Keep the last potentially incomplete line in the buffer
|
|
83
|
+
buffer = lines.pop() || '';
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith('data: ')) {
|
|
87
|
+
const data = line.slice(6).trim();
|
|
88
|
+
if (data === '[DONE]') continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(data);
|
|
92
|
+
if (parsed.response) {
|
|
93
|
+
assistantContent += parsed.response;
|
|
94
|
+
assistantEl.textContent = assistantContent;
|
|
95
|
+
assistantEl.className = 'message assistant';
|
|
96
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Skip malformed JSON chunks
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Process any remaining buffer content
|
|
106
|
+
if (buffer.startsWith('data: ')) {
|
|
107
|
+
const data = buffer.slice(6).trim();
|
|
108
|
+
if (data && data !== '[DONE]') {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(data);
|
|
111
|
+
if (parsed.response) {
|
|
112
|
+
assistantContent += parsed.response;
|
|
113
|
+
assistantEl.textContent = assistantContent;
|
|
114
|
+
assistantEl.className = 'message assistant';
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip malformed JSON
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Save to history if we got content
|
|
123
|
+
if (assistantContent) {
|
|
124
|
+
history.push({ role: 'assistant', content: assistantContent });
|
|
125
|
+
} else {
|
|
126
|
+
assistantEl.textContent = 'No response received. Please try again.';
|
|
127
|
+
assistantEl.className = 'message assistant error';
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('Chat error:', err);
|
|
131
|
+
assistantEl.textContent = 'Connection error. Please check your network and try again.';
|
|
132
|
+
assistantEl.className = 'message assistant error';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setLoading(false);
|
|
136
|
+
inputEl.focus();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Event listeners
|
|
140
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
141
|
+
inputEl.addEventListener('keypress', (e) => {
|
|
142
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
sendMessage();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Focus input on load
|
|
149
|
+
inputEl.focus();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Chat</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
16
|
+
background: #f5f5f5;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
display: flex;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
padding: 1rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.chat-container {
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-width: 700px;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
height: calc(100vh - 2rem);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
header {
|
|
32
|
+
text-align: center;
|
|
33
|
+
padding: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
header h1 {
|
|
37
|
+
font-size: 1.5rem;
|
|
38
|
+
color: #333;
|
|
39
|
+
margin-bottom: 0.25rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
header p {
|
|
43
|
+
font-size: 0.875rem;
|
|
44
|
+
color: #666;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.messages {
|
|
48
|
+
flex: 1;
|
|
49
|
+
overflow-y: auto;
|
|
50
|
+
padding: 1rem;
|
|
51
|
+
background: white;
|
|
52
|
+
border-radius: 12px;
|
|
53
|
+
margin-bottom: 1rem;
|
|
54
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.message {
|
|
58
|
+
padding: 0.75rem 1rem;
|
|
59
|
+
margin-bottom: 0.75rem;
|
|
60
|
+
border-radius: 12px;
|
|
61
|
+
max-width: 85%;
|
|
62
|
+
line-height: 1.5;
|
|
63
|
+
word-wrap: break-word;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.message.user {
|
|
67
|
+
background: #007bff;
|
|
68
|
+
color: white;
|
|
69
|
+
margin-left: auto;
|
|
70
|
+
border-bottom-right-radius: 4px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.message.assistant {
|
|
74
|
+
background: #e9ecef;
|
|
75
|
+
color: #333;
|
|
76
|
+
margin-right: auto;
|
|
77
|
+
border-bottom-left-radius: 4px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.message.error {
|
|
81
|
+
background: #fee;
|
|
82
|
+
color: #c00;
|
|
83
|
+
border: 1px solid #fcc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.message.typing {
|
|
87
|
+
color: #666;
|
|
88
|
+
font-style: italic;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.input-area {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 0.5rem;
|
|
94
|
+
background: white;
|
|
95
|
+
padding: 1rem;
|
|
96
|
+
border-radius: 12px;
|
|
97
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.input-area input {
|
|
101
|
+
flex: 1;
|
|
102
|
+
padding: 0.75rem 1rem;
|
|
103
|
+
border: 1px solid #ddd;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
font-size: 1rem;
|
|
106
|
+
outline: none;
|
|
107
|
+
transition: border-color 0.2s;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.input-area input:focus {
|
|
111
|
+
border-color: #007bff;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.input-area input:disabled {
|
|
115
|
+
background: #f5f5f5;
|
|
116
|
+
cursor: not-allowed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.input-area button {
|
|
120
|
+
padding: 0.75rem 1.5rem;
|
|
121
|
+
background: #007bff;
|
|
122
|
+
color: white;
|
|
123
|
+
border: none;
|
|
124
|
+
border-radius: 8px;
|
|
125
|
+
font-size: 1rem;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
transition: background 0.2s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.input-area button:hover:not(:disabled) {
|
|
131
|
+
background: #0056b3;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.input-area button:disabled {
|
|
135
|
+
background: #ccc;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.empty-state {
|
|
140
|
+
text-align: center;
|
|
141
|
+
color: #999;
|
|
142
|
+
padding: 3rem 1rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.empty-state p {
|
|
146
|
+
font-size: 1.1rem;
|
|
147
|
+
margin-bottom: 0.5rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.empty-state small {
|
|
151
|
+
font-size: 0.875rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (max-width: 480px) {
|
|
155
|
+
body {
|
|
156
|
+
padding: 0.5rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.chat-container {
|
|
160
|
+
height: calc(100vh - 1rem);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
header h1 {
|
|
164
|
+
font-size: 1.25rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.message {
|
|
168
|
+
max-width: 90%;
|
|
169
|
+
padding: 0.625rem 0.875rem;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.input-area {
|
|
173
|
+
padding: 0.75rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.input-area button {
|
|
177
|
+
padding: 0.75rem 1rem;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<div class="chat-container">
|
|
184
|
+
<header>
|
|
185
|
+
<h1>AI Chat</h1>
|
|
186
|
+
<p>Powered by Cloudflare AI (Mistral 7B)</p>
|
|
187
|
+
</header>
|
|
188
|
+
|
|
189
|
+
<div id="messages" class="messages">
|
|
190
|
+
<div class="empty-state">
|
|
191
|
+
<p>Start a conversation</p>
|
|
192
|
+
<small>Type a message below to begin</small>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="input-area">
|
|
197
|
+
<input
|
|
198
|
+
type="text"
|
|
199
|
+
id="input"
|
|
200
|
+
placeholder="Type your message..."
|
|
201
|
+
autocomplete="off"
|
|
202
|
+
/>
|
|
203
|
+
<button id="send">Send</button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<script src="/chat.js"></script>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
interface Env {
|
|
2
|
+
AI: Ai;
|
|
3
|
+
ASSETS: Fetcher;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
role: "user" | "assistant" | "system";
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Rate limiting: 10 requests per minute per IP
|
|
12
|
+
const RATE_LIMIT = 10;
|
|
13
|
+
const RATE_WINDOW_MS = 60_000;
|
|
14
|
+
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
|
15
|
+
|
|
16
|
+
function checkRateLimit(ip: string): boolean {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const entry = rateLimitMap.get(ip);
|
|
19
|
+
|
|
20
|
+
if (!entry || now >= entry.resetAt) {
|
|
21
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (entry.count >= RATE_LIMIT) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
entry.count++;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Clean up old entries periodically to prevent memory leaks
|
|
34
|
+
function cleanupRateLimitMap(): void {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [ip, entry] of rateLimitMap) {
|
|
37
|
+
if (now >= entry.resetAt) {
|
|
38
|
+
rateLimitMap.delete(ip);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
|
|
47
|
+
// Serve static assets for non-API routes
|
|
48
|
+
if (request.method === "GET" && !url.pathname.startsWith("/api")) {
|
|
49
|
+
return env.ASSETS.fetch(request);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// POST /api/chat - Streaming chat endpoint
|
|
53
|
+
if (request.method === "POST" && url.pathname === "/api/chat") {
|
|
54
|
+
const ip = request.headers.get("cf-connecting-ip") || "unknown";
|
|
55
|
+
|
|
56
|
+
// Check rate limit
|
|
57
|
+
if (!checkRateLimit(ip)) {
|
|
58
|
+
// Cleanup old entries occasionally
|
|
59
|
+
cleanupRateLimitMap();
|
|
60
|
+
return Response.json(
|
|
61
|
+
{ error: "Too many requests. Please wait a moment and try again." },
|
|
62
|
+
{ status: 429 },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const body = (await request.json()) as { messages?: ChatMessage[] };
|
|
68
|
+
const messages = body.messages;
|
|
69
|
+
|
|
70
|
+
if (!messages || !Array.isArray(messages)) {
|
|
71
|
+
return Response.json(
|
|
72
|
+
{ error: "Invalid request. Please provide a messages array." },
|
|
73
|
+
{ status: 400 },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Stream response using SSE
|
|
78
|
+
const stream = await env.AI.run(
|
|
79
|
+
"@cf/mistral/mistral-7b-instruct-v0.1",
|
|
80
|
+
{
|
|
81
|
+
messages,
|
|
82
|
+
stream: true,
|
|
83
|
+
max_tokens: 1024,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return new Response(stream, {
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "text/event-stream",
|
|
90
|
+
"Cache-Control": "no-cache",
|
|
91
|
+
Connection: "keep-alive",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error("Chat error:", err);
|
|
96
|
+
return Response.json(
|
|
97
|
+
{ error: "Something went wrong. Please try again." },
|
|
98
|
+
{ status: 500 },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "semantic-search",
|
|
3
|
+
"description": "Semantic search with AI embeddings + Vectorize",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"capabilities": ["ai", "db"],
|
|
6
|
+
"intent": {
|
|
7
|
+
"keywords": ["rag", "semantic", "search", "embeddings", "vectorize", "vector", "similarity"],
|
|
8
|
+
"examples": ["RAG app", "document search", "semantic search", "knowledge base"]
|
|
9
|
+
},
|
|
10
|
+
"hooks": {
|
|
11
|
+
"postDeploy": [
|
|
12
|
+
{ "action": "clipboard", "text": "{{url}}", "message": "URL copied" },
|
|
13
|
+
{
|
|
14
|
+
"action": "box",
|
|
15
|
+
"title": "Semantic Search: {{name}}",
|
|
16
|
+
"lines": [
|
|
17
|
+
"{{url}}",
|
|
18
|
+
"",
|
|
19
|
+
"Open in browser to index documents and search!",
|
|
20
|
+
"",
|
|
21
|
+
"Note: Local dev requires 'wrangler dev --remote'"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "jack-template",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
9
|
+
"typescript": "^5.0.0",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260120.0", "", {}, "sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw=="],
|
|
15
|
+
|
|
16
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const docIdEl = document.getElementById("doc-id");
|
|
2
|
+
const docContentEl = document.getElementById("doc-content");
|
|
3
|
+
const indexBtn = document.getElementById("index-btn");
|
|
4
|
+
const indexStatus = document.getElementById("index-status");
|
|
5
|
+
const searchQueryEl = document.getElementById("search-query");
|
|
6
|
+
const searchBtn = document.getElementById("search-btn");
|
|
7
|
+
const resultsEl = document.getElementById("results");
|
|
8
|
+
|
|
9
|
+
// Index document
|
|
10
|
+
async function indexDocument() {
|
|
11
|
+
const id = docIdEl.value.trim();
|
|
12
|
+
const content = docContentEl.value.trim();
|
|
13
|
+
|
|
14
|
+
if (!id || !content) {
|
|
15
|
+
showStatus(indexStatus, "Please enter both ID and content", "error");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
indexBtn.disabled = true;
|
|
20
|
+
indexBtn.innerHTML = '<span class="loading"></span>Indexing...';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch("/api/index", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ id, content }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
|
|
31
|
+
if (response.ok) {
|
|
32
|
+
showStatus(indexStatus, `Document "${id}" indexed successfully!`, "success");
|
|
33
|
+
docIdEl.value = "";
|
|
34
|
+
docContentEl.value = "";
|
|
35
|
+
} else {
|
|
36
|
+
showStatus(indexStatus, data.error || "Failed to index document", "error");
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
showStatus(indexStatus, "Network error. Please try again.", "error");
|
|
40
|
+
} finally {
|
|
41
|
+
indexBtn.disabled = false;
|
|
42
|
+
indexBtn.textContent = "Index Document";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Search documents
|
|
47
|
+
async function searchDocuments() {
|
|
48
|
+
const query = searchQueryEl.value.trim();
|
|
49
|
+
|
|
50
|
+
if (!query) {
|
|
51
|
+
resultsEl.innerHTML = '<div class="no-results">Please enter a search query</div>';
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
searchBtn.disabled = true;
|
|
56
|
+
searchBtn.innerHTML = '<span class="loading"></span>Searching...';
|
|
57
|
+
resultsEl.innerHTML = "";
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch("/api/search", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ query, limit: 5 }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
if (data.results && data.results.length > 0) {
|
|
70
|
+
resultsEl.innerHTML = data.results
|
|
71
|
+
.map(
|
|
72
|
+
(result) => `
|
|
73
|
+
<div class="result">
|
|
74
|
+
<div class="result-header">
|
|
75
|
+
<span class="result-id">${escapeHtml(result.id)}</span>
|
|
76
|
+
<span class="result-score">Score: ${(result.score * 100).toFixed(1)}%</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="result-preview">${escapeHtml(result.metadata?.preview || "No preview available")}</div>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
)
|
|
82
|
+
.join("");
|
|
83
|
+
} else {
|
|
84
|
+
resultsEl.innerHTML = '<div class="no-results">No matching documents found</div>';
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
resultsEl.innerHTML = `<div class="no-results">${escapeHtml(data.error || "Search failed")}</div>`;
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
resultsEl.innerHTML = '<div class="no-results">Network error. Please try again.</div>';
|
|
91
|
+
} finally {
|
|
92
|
+
searchBtn.disabled = false;
|
|
93
|
+
searchBtn.textContent = "Search";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper functions
|
|
98
|
+
function showStatus(el, message, type) {
|
|
99
|
+
el.textContent = message;
|
|
100
|
+
el.className = `status ${type}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapeHtml(text) {
|
|
104
|
+
const div = document.createElement("div");
|
|
105
|
+
div.textContent = text;
|
|
106
|
+
return div.innerHTML;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Event listeners
|
|
110
|
+
indexBtn.addEventListener("click", indexDocument);
|
|
111
|
+
searchBtn.addEventListener("click", searchDocuments);
|
|
112
|
+
|
|
113
|
+
// Enter key support
|
|
114
|
+
docIdEl.addEventListener("keypress", (e) => {
|
|
115
|
+
if (e.key === "Enter") docContentEl.focus();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
searchQueryEl.addEventListener("keypress", (e) => {
|
|
119
|
+
if (e.key === "Enter") searchDocuments();
|
|
120
|
+
});
|