genai-fs 1.0.2
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/.claude/settings.local.json +15 -0
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/CLAUDE.md +51 -0
- package/README.md +104 -0
- package/bun.lock +244 -0
- package/package.json +39 -0
- package/src/App.tsx +492 -0
- package/src/components/DocumentDeleteConfirm.tsx +27 -0
- package/src/components/DocumentDetail.tsx +94 -0
- package/src/components/DocumentList.tsx +98 -0
- package/src/components/FileBrowser.tsx +173 -0
- package/src/components/Spinner.tsx +26 -0
- package/src/components/StatusScreen.tsx +22 -0
- package/src/components/StoreDetail.tsx +80 -0
- package/src/components/StoreForm.tsx +38 -0
- package/src/components/StoreList.tsx +80 -0
- package/src/index.tsx +5 -0
- package/src/lib/api.ts +191 -0
- package/src/utils/formatters.ts +44 -0
- package/tsconfig.json +29 -0
package/src/App.tsx
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
3
|
+
import {
|
|
4
|
+
listStores,
|
|
5
|
+
listDocuments,
|
|
6
|
+
uploadFile,
|
|
7
|
+
deleteDocument,
|
|
8
|
+
deleteStore,
|
|
9
|
+
createStore,
|
|
10
|
+
type FileSearchStore,
|
|
11
|
+
type Document,
|
|
12
|
+
} from "./lib/api.ts";
|
|
13
|
+
import { FileBrowser } from "./components/FileBrowser.tsx";
|
|
14
|
+
import { StatusScreen } from "./components/StatusScreen.tsx";
|
|
15
|
+
import { StoreList } from "./components/StoreList.tsx";
|
|
16
|
+
import { StoreDetail } from "./components/StoreDetail.tsx";
|
|
17
|
+
import { StoreForm } from "./components/StoreForm.tsx";
|
|
18
|
+
import { DocumentList } from "./components/DocumentList.tsx";
|
|
19
|
+
import { DocumentDetail } from "./components/DocumentDetail.tsx";
|
|
20
|
+
import { DocumentDeleteConfirm } from "./components/DocumentDeleteConfirm.tsx";
|
|
21
|
+
import { maskApiKey } from "./utils/formatters.ts";
|
|
22
|
+
|
|
23
|
+
// 画面の種類
|
|
24
|
+
type Screen =
|
|
25
|
+
| "api-key-input"
|
|
26
|
+
| "store-select"
|
|
27
|
+
| "store-detail"
|
|
28
|
+
| "document-list"
|
|
29
|
+
| "document-detail"
|
|
30
|
+
| "file-browser"
|
|
31
|
+
| "uploading"
|
|
32
|
+
| "confirm-delete"
|
|
33
|
+
| "deleting"
|
|
34
|
+
| "confirm-store-delete"
|
|
35
|
+
| "store-deleting"
|
|
36
|
+
| "store-create"
|
|
37
|
+
| "store-creating";
|
|
38
|
+
|
|
39
|
+
export function App() {
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
|
|
42
|
+
// APIキーの状態(環境変数から初期化)
|
|
43
|
+
const [apiKey, setApiKey] = useState<string>(process.env.GEMINI_API_KEY || "");
|
|
44
|
+
const [apiKeyInput, setApiKeyInput] = useState("");
|
|
45
|
+
|
|
46
|
+
// 初期画面を決定
|
|
47
|
+
const [screen, setScreen] = useState<Screen>(
|
|
48
|
+
process.env.GEMINI_API_KEY ? "store-select" : "api-key-input"
|
|
49
|
+
);
|
|
50
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
51
|
+
const [selectedStore, setSelectedStore] = useState<FileSearchStore | null>(null);
|
|
52
|
+
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
|
|
53
|
+
|
|
54
|
+
// ストア一覧の状態
|
|
55
|
+
const [stores, setStores] = useState<FileSearchStore[]>([]);
|
|
56
|
+
const [loading, setLoading] = useState(!!process.env.GEMINI_API_KEY);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
// ドキュメント一覧の状態
|
|
60
|
+
const [documents, setDocuments] = useState<Document[]>([]);
|
|
61
|
+
|
|
62
|
+
// アップロード状態
|
|
63
|
+
const [uploadStatus, setUploadStatus] = useState<string>("");
|
|
64
|
+
const [isUploading, setIsUploading] = useState(true);
|
|
65
|
+
|
|
66
|
+
// 削除状態
|
|
67
|
+
const [deleteStatus, setDeleteStatus] = useState<string>("");
|
|
68
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
69
|
+
|
|
70
|
+
// ストア削除状態
|
|
71
|
+
const [storeDeleteInput, setStoreDeleteInput] = useState("");
|
|
72
|
+
const [storeDeleteStatus, setStoreDeleteStatus] = useState<string>("");
|
|
73
|
+
const [isStoreDeleting, setIsStoreDeleting] = useState(false);
|
|
74
|
+
|
|
75
|
+
// ストア作成状態
|
|
76
|
+
const [storeCreateInput, setStoreCreateInput] = useState("");
|
|
77
|
+
const [storeCreateStatus, setStoreCreateStatus] = useState<string>("");
|
|
78
|
+
const [isStoreCreating, setIsStoreCreating] = useState(false);
|
|
79
|
+
|
|
80
|
+
// ドキュメント一覧を再取得
|
|
81
|
+
const refreshDocuments = useCallback(async () => {
|
|
82
|
+
if (!selectedStore) return;
|
|
83
|
+
try {
|
|
84
|
+
setLoading(true);
|
|
85
|
+
setError(null);
|
|
86
|
+
const result = await listDocuments(selectedStore.name);
|
|
87
|
+
setDocuments(result);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
setError(e instanceof Error ? e.message : "Unknown error");
|
|
90
|
+
} finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}, [selectedStore]);
|
|
94
|
+
|
|
95
|
+
// ストア一覧を取得
|
|
96
|
+
const fetchStores = useCallback(async () => {
|
|
97
|
+
if (!apiKey) return;
|
|
98
|
+
try {
|
|
99
|
+
setLoading(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
const result = await listStores();
|
|
102
|
+
setStores(result);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
setError(e instanceof Error ? e.message : "Unknown error");
|
|
105
|
+
} finally {
|
|
106
|
+
setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}, [apiKey]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
fetchStores();
|
|
112
|
+
}, [fetchStores]);
|
|
113
|
+
|
|
114
|
+
// APIキー設定後にストア一覧を取得
|
|
115
|
+
const handleApiKeySubmit = () => {
|
|
116
|
+
if (!apiKeyInput.trim()) return;
|
|
117
|
+
process.env.GEMINI_API_KEY = apiKeyInput.trim();
|
|
118
|
+
setApiKey(apiKeyInput.trim());
|
|
119
|
+
setApiKeyInput("");
|
|
120
|
+
setScreen("store-select");
|
|
121
|
+
setLoading(true);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ストア選択時にドキュメント一覧を取得
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
refreshDocuments();
|
|
127
|
+
}, [refreshDocuments]);
|
|
128
|
+
|
|
129
|
+
// ファイル選択時のハンドラ
|
|
130
|
+
const handleFileSelect = async (filePath: string) => {
|
|
131
|
+
if (!selectedStore) return;
|
|
132
|
+
|
|
133
|
+
setScreen("uploading");
|
|
134
|
+
setIsUploading(true);
|
|
135
|
+
setUploadStatus(`Uploading: ${filePath.split("/").pop()}`);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await uploadFile(selectedStore.name, filePath);
|
|
139
|
+
setIsUploading(false);
|
|
140
|
+
setUploadStatus("Upload complete!");
|
|
141
|
+
|
|
142
|
+
// 少し待ってから一覧を更新
|
|
143
|
+
setTimeout(async () => {
|
|
144
|
+
await refreshDocuments();
|
|
145
|
+
setScreen("document-list");
|
|
146
|
+
setSelectedIndex(0);
|
|
147
|
+
}, 1500);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
setIsUploading(false);
|
|
150
|
+
setUploadStatus(`Error: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
setScreen("document-list");
|
|
153
|
+
}, 3000);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleFileBrowserCancel = () => {
|
|
158
|
+
setScreen("document-list");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ドキュメント削除ハンドラ
|
|
162
|
+
const handleDeleteDocument = async () => {
|
|
163
|
+
if (!selectedDocument) return;
|
|
164
|
+
|
|
165
|
+
setScreen("deleting");
|
|
166
|
+
setIsDeleting(true);
|
|
167
|
+
setDeleteStatus(`Deleting: ${selectedDocument.displayName || selectedDocument.name.split("/").pop()}`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await deleteDocument(selectedDocument.name);
|
|
171
|
+
setIsDeleting(false);
|
|
172
|
+
setDeleteStatus("Delete complete!");
|
|
173
|
+
|
|
174
|
+
setTimeout(async () => {
|
|
175
|
+
await refreshDocuments();
|
|
176
|
+
setSelectedDocument(null);
|
|
177
|
+
setScreen("document-list");
|
|
178
|
+
setSelectedIndex(0);
|
|
179
|
+
}, 1000);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
setIsDeleting(false);
|
|
182
|
+
setDeleteStatus(`Error: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
setScreen("document-detail");
|
|
185
|
+
}, 3000);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// ストア削除ハンドラ
|
|
190
|
+
const handleDeleteStore = async () => {
|
|
191
|
+
const store = stores[selectedIndex];
|
|
192
|
+
if (!store) return;
|
|
193
|
+
|
|
194
|
+
setScreen("store-deleting");
|
|
195
|
+
setIsStoreDeleting(true);
|
|
196
|
+
setStoreDeleteStatus(`Deleting: ${store.displayName}`);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await deleteStore(store.name);
|
|
200
|
+
setIsStoreDeleting(false);
|
|
201
|
+
setStoreDeleteStatus("Delete complete!");
|
|
202
|
+
|
|
203
|
+
setTimeout(async () => {
|
|
204
|
+
// ストア一覧を再取得
|
|
205
|
+
try {
|
|
206
|
+
setLoading(true);
|
|
207
|
+
const result = await listStores();
|
|
208
|
+
setStores(result);
|
|
209
|
+
setSelectedIndex(0);
|
|
210
|
+
} finally {
|
|
211
|
+
setLoading(false);
|
|
212
|
+
}
|
|
213
|
+
setScreen("store-select");
|
|
214
|
+
setStoreDeleteInput("");
|
|
215
|
+
}, 1000);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
setIsStoreDeleting(false);
|
|
218
|
+
setStoreDeleteStatus(`Error: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
setScreen("store-select");
|
|
221
|
+
setStoreDeleteInput("");
|
|
222
|
+
}, 3000);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ストア作成ハンドラ
|
|
227
|
+
const handleCreateStore = async () => {
|
|
228
|
+
if (!storeCreateInput.trim()) return;
|
|
229
|
+
|
|
230
|
+
setScreen("store-creating");
|
|
231
|
+
setIsStoreCreating(true);
|
|
232
|
+
setStoreCreateStatus(`Creating: ${storeCreateInput}`);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await createStore(storeCreateInput.trim());
|
|
236
|
+
setIsStoreCreating(false);
|
|
237
|
+
setStoreCreateStatus("Create complete!");
|
|
238
|
+
|
|
239
|
+
setTimeout(async () => {
|
|
240
|
+
// ストア一覧を再取得
|
|
241
|
+
try {
|
|
242
|
+
setLoading(true);
|
|
243
|
+
const result = await listStores();
|
|
244
|
+
setStores(result);
|
|
245
|
+
setSelectedIndex(0);
|
|
246
|
+
} finally {
|
|
247
|
+
setLoading(false);
|
|
248
|
+
}
|
|
249
|
+
setScreen("store-select");
|
|
250
|
+
setStoreCreateInput("");
|
|
251
|
+
}, 1000);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
setIsStoreCreating(false);
|
|
254
|
+
setStoreCreateStatus(`Error: ${e instanceof Error ? e.message : "Unknown error"}`);
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
setScreen("store-select");
|
|
257
|
+
setStoreCreateInput("");
|
|
258
|
+
}, 3000);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// テキスト入力処理
|
|
263
|
+
const handleTextInput = (
|
|
264
|
+
input: string,
|
|
265
|
+
key: { escape: boolean; backspace: boolean; delete: boolean; return: boolean; ctrl: boolean; meta: boolean },
|
|
266
|
+
setInput: (fn: (prev: string) => string) => void,
|
|
267
|
+
onSubmit: () => void,
|
|
268
|
+
onCancel: () => void
|
|
269
|
+
) => {
|
|
270
|
+
if (key.escape) { onCancel(); return; }
|
|
271
|
+
if (key.backspace || key.delete) { setInput((prev) => prev.slice(0, -1)); return; }
|
|
272
|
+
if (key.return) { onSubmit(); return; }
|
|
273
|
+
if (input && !key.ctrl && !key.meta) setInput((prev) => prev + input);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
useInput((input, key) => {
|
|
277
|
+
// qキーは共通で終了(入力画面以外)
|
|
278
|
+
if (input === "q" && !["api-key-input", "store-create", "confirm-store-delete"].includes(screen)) {
|
|
279
|
+
exit();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
switch (screen) {
|
|
284
|
+
// APIキー入力画面
|
|
285
|
+
case "api-key-input":
|
|
286
|
+
handleTextInput(
|
|
287
|
+
input,
|
|
288
|
+
key,
|
|
289
|
+
setApiKeyInput,
|
|
290
|
+
handleApiKeySubmit,
|
|
291
|
+
exit
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
|
|
295
|
+
// 入力を無視する画面
|
|
296
|
+
case "file-browser":
|
|
297
|
+
case "uploading":
|
|
298
|
+
case "store-creating":
|
|
299
|
+
case "store-deleting":
|
|
300
|
+
case "deleting":
|
|
301
|
+
return;
|
|
302
|
+
|
|
303
|
+
// ストア作成画面
|
|
304
|
+
case "store-create":
|
|
305
|
+
handleTextInput(
|
|
306
|
+
input,
|
|
307
|
+
key,
|
|
308
|
+
setStoreCreateInput,
|
|
309
|
+
() => { if (storeCreateInput.trim()) handleCreateStore(); },
|
|
310
|
+
() => { setScreen("store-select"); setStoreCreateInput(""); }
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
|
|
314
|
+
// ストア削除確認画面
|
|
315
|
+
case "confirm-store-delete": {
|
|
316
|
+
const store = stores[selectedIndex];
|
|
317
|
+
handleTextInput(
|
|
318
|
+
input,
|
|
319
|
+
key,
|
|
320
|
+
setStoreDeleteInput,
|
|
321
|
+
() => { if (store && storeDeleteInput === store.displayName) handleDeleteStore(); },
|
|
322
|
+
() => { setScreen("store-select"); setStoreDeleteInput(""); }
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ドキュメント削除確認画面
|
|
328
|
+
case "confirm-delete":
|
|
329
|
+
if (input === "y" || input === "Y") handleDeleteDocument();
|
|
330
|
+
if (input === "n" || input === "N" || key.escape) setScreen("document-detail");
|
|
331
|
+
return;
|
|
332
|
+
|
|
333
|
+
// ストア詳細画面
|
|
334
|
+
case "store-detail":
|
|
335
|
+
if (key.escape || input === "b") setScreen("store-select");
|
|
336
|
+
return;
|
|
337
|
+
|
|
338
|
+
// ドキュメント詳細画面
|
|
339
|
+
case "document-detail":
|
|
340
|
+
if (key.escape || input === "b") {
|
|
341
|
+
setScreen("document-list");
|
|
342
|
+
setSelectedDocument(null);
|
|
343
|
+
}
|
|
344
|
+
if (input === "d") setScreen("confirm-delete");
|
|
345
|
+
return;
|
|
346
|
+
|
|
347
|
+
// ストア一覧画面
|
|
348
|
+
case "store-select":
|
|
349
|
+
if (loading) return;
|
|
350
|
+
if (key.upArrow) setSelectedIndex((prev) => (prev > 0 ? prev - 1 : stores.length - 1));
|
|
351
|
+
if (key.downArrow) setSelectedIndex((prev) => (prev < stores.length - 1 ? prev + 1 : 0));
|
|
352
|
+
if (key.return && stores[selectedIndex]) {
|
|
353
|
+
setSelectedStore(stores[selectedIndex]);
|
|
354
|
+
setScreen("document-list");
|
|
355
|
+
setSelectedIndex(0);
|
|
356
|
+
}
|
|
357
|
+
if (input === "i" && stores.length > 0) setScreen("store-detail");
|
|
358
|
+
if (input === "n") { setScreen("store-create"); setStoreCreateInput(""); }
|
|
359
|
+
if (input === "d" && stores.length > 0) { setScreen("confirm-store-delete"); setStoreDeleteInput(""); }
|
|
360
|
+
return;
|
|
361
|
+
|
|
362
|
+
// ドキュメント一覧画面
|
|
363
|
+
case "document-list":
|
|
364
|
+
if (loading) return;
|
|
365
|
+
if (key.upArrow) setSelectedIndex((prev) => (prev > 0 ? prev - 1 : documents.length - 1));
|
|
366
|
+
if (key.downArrow) setSelectedIndex((prev) => (prev < documents.length - 1 ? prev + 1 : 0));
|
|
367
|
+
if ((key.return || input === "i") && documents[selectedIndex]) {
|
|
368
|
+
setSelectedDocument(documents[selectedIndex]);
|
|
369
|
+
setScreen("document-detail");
|
|
370
|
+
}
|
|
371
|
+
if (input === "u") setScreen("file-browser");
|
|
372
|
+
if (key.escape) {
|
|
373
|
+
setScreen("store-select");
|
|
374
|
+
setSelectedStore(null);
|
|
375
|
+
setDocuments([]);
|
|
376
|
+
setSelectedIndex(0);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<Box flexDirection="column" padding={1}>
|
|
384
|
+
<Box marginBottom={1}>
|
|
385
|
+
<Text bold color="cyan">
|
|
386
|
+
Gemini FileStore Manager
|
|
387
|
+
</Text>
|
|
388
|
+
</Box>
|
|
389
|
+
|
|
390
|
+
<Box marginBottom={1}>
|
|
391
|
+
<Text dimColor>{maskApiKey()}</Text>
|
|
392
|
+
{selectedStore && (
|
|
393
|
+
<>
|
|
394
|
+
<Text dimColor> / </Text>
|
|
395
|
+
<Text color="yellow">{selectedStore.displayName}</Text>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
{selectedDocument && (
|
|
399
|
+
<>
|
|
400
|
+
<Text dimColor> / </Text>
|
|
401
|
+
<Text color="yellow">
|
|
402
|
+
{selectedDocument.displayName || selectedDocument.name.split("/").pop()}
|
|
403
|
+
</Text>
|
|
404
|
+
</>
|
|
405
|
+
)}
|
|
406
|
+
</Box>
|
|
407
|
+
|
|
408
|
+
{screen === "api-key-input" && (
|
|
409
|
+
<Box flexDirection="column">
|
|
410
|
+
<Box marginBottom={1}>
|
|
411
|
+
<Text color="yellow" bold>
|
|
412
|
+
GEMINI_API_KEY is not set
|
|
413
|
+
</Text>
|
|
414
|
+
</Box>
|
|
415
|
+
<Text>Enter your Gemini API key:</Text>
|
|
416
|
+
<Box marginTop={1}>
|
|
417
|
+
<Text dimColor>{">"} </Text>
|
|
418
|
+
<Text color={apiKeyInput.trim() ? "green" : "white"}>
|
|
419
|
+
{apiKeyInput ? "*".repeat(apiKeyInput.length) : ""}
|
|
420
|
+
</Text>
|
|
421
|
+
<Text color="gray">|</Text>
|
|
422
|
+
</Box>
|
|
423
|
+
<Box marginTop={1}>
|
|
424
|
+
<Text dimColor>Enter: Submit Esc: Quit</Text>
|
|
425
|
+
</Box>
|
|
426
|
+
</Box>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{screen === "store-select" && (
|
|
430
|
+
<StoreList
|
|
431
|
+
stores={stores}
|
|
432
|
+
selectedIndex={selectedIndex}
|
|
433
|
+
loading={loading}
|
|
434
|
+
error={error}
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{screen === "store-detail" && stores[selectedIndex] && (
|
|
439
|
+
<StoreDetail store={stores[selectedIndex]} />
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{screen === "store-create" && (
|
|
443
|
+
<StoreForm mode="create" input={storeCreateInput} />
|
|
444
|
+
)}
|
|
445
|
+
|
|
446
|
+
{screen === "store-creating" && (
|
|
447
|
+
<StatusScreen isProcessing={isStoreCreating} message={storeCreateStatus} />
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{screen === "confirm-store-delete" && stores[selectedIndex] && (
|
|
451
|
+
<StoreForm
|
|
452
|
+
mode="delete"
|
|
453
|
+
input={storeDeleteInput}
|
|
454
|
+
storeName={stores[selectedIndex].displayName}
|
|
455
|
+
/>
|
|
456
|
+
)}
|
|
457
|
+
|
|
458
|
+
{screen === "store-deleting" && (
|
|
459
|
+
<StatusScreen isProcessing={isStoreDeleting} message={storeDeleteStatus} />
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{screen === "document-list" && (
|
|
463
|
+
<DocumentList
|
|
464
|
+
documents={documents}
|
|
465
|
+
selectedIndex={selectedIndex}
|
|
466
|
+
loading={loading}
|
|
467
|
+
error={error}
|
|
468
|
+
/>
|
|
469
|
+
)}
|
|
470
|
+
|
|
471
|
+
{screen === "document-detail" && selectedDocument && (
|
|
472
|
+
<DocumentDetail document={selectedDocument} />
|
|
473
|
+
)}
|
|
474
|
+
|
|
475
|
+
{screen === "confirm-delete" && selectedDocument && (
|
|
476
|
+
<DocumentDeleteConfirm document={selectedDocument} />
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{screen === "deleting" && (
|
|
480
|
+
<StatusScreen isProcessing={isDeleting} message={deleteStatus} />
|
|
481
|
+
)}
|
|
482
|
+
|
|
483
|
+
{screen === "file-browser" && (
|
|
484
|
+
<FileBrowser onSelect={handleFileSelect} onCancel={handleFileBrowserCancel} />
|
|
485
|
+
)}
|
|
486
|
+
|
|
487
|
+
{screen === "uploading" && (
|
|
488
|
+
<StatusScreen isProcessing={isUploading} message={uploadStatus} />
|
|
489
|
+
)}
|
|
490
|
+
</Box>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { Document } from "../lib/api.ts";
|
|
3
|
+
|
|
4
|
+
interface DocumentDeleteConfirmProps {
|
|
5
|
+
document: Document;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Document削除確認ダイアログ
|
|
9
|
+
export function DocumentDeleteConfirm({
|
|
10
|
+
document,
|
|
11
|
+
}: DocumentDeleteConfirmProps) {
|
|
12
|
+
const name = document.displayName || document.name.split("/").pop();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box flexDirection="column">
|
|
16
|
+
<Box marginBottom={1}>
|
|
17
|
+
<Text color="red" bold>
|
|
18
|
+
Delete this document?
|
|
19
|
+
</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
<Text>{name}</Text>
|
|
22
|
+
<Box marginTop={1}>
|
|
23
|
+
<Text dimColor>y: Delete n/Esc: Cancel</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
</Box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { Document } from "../lib/api.ts";
|
|
3
|
+
import { formatBytes, formatDate, formatState } from "../utils/formatters.ts";
|
|
4
|
+
|
|
5
|
+
interface DocumentDetailProps {
|
|
6
|
+
document: Document;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Document詳細表示
|
|
10
|
+
export function DocumentDetail({ document }: DocumentDetailProps) {
|
|
11
|
+
const state = formatState(document.state);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Box flexDirection="column" gap={0}>
|
|
15
|
+
<Box>
|
|
16
|
+
<Box width={14}>
|
|
17
|
+
<Text bold dimColor>
|
|
18
|
+
Name
|
|
19
|
+
</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
<Text>{document.displayName || "-"}</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
<Box>
|
|
24
|
+
<Box width={14}>
|
|
25
|
+
<Text bold dimColor>
|
|
26
|
+
ID
|
|
27
|
+
</Text>
|
|
28
|
+
</Box>
|
|
29
|
+
<Text>{document.name.split("/").pop()}</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
<Box>
|
|
32
|
+
<Box width={14}>
|
|
33
|
+
<Text bold dimColor>
|
|
34
|
+
MIME Type
|
|
35
|
+
</Text>
|
|
36
|
+
</Box>
|
|
37
|
+
<Text>{document.mimeType || "-"}</Text>
|
|
38
|
+
</Box>
|
|
39
|
+
<Box>
|
|
40
|
+
<Box width={14}>
|
|
41
|
+
<Text bold dimColor>
|
|
42
|
+
Size
|
|
43
|
+
</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
<Text>{formatBytes(document.sizeBytes)}</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
<Box>
|
|
48
|
+
<Box width={14}>
|
|
49
|
+
<Text bold dimColor>
|
|
50
|
+
State
|
|
51
|
+
</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
<Text color={state.color}>{state.text}</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
<Box>
|
|
56
|
+
<Box width={14}>
|
|
57
|
+
<Text bold dimColor>
|
|
58
|
+
Created
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
<Text>{formatDate(document.createTime)}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
<Box>
|
|
64
|
+
<Box width={14}>
|
|
65
|
+
<Text bold dimColor>
|
|
66
|
+
Updated
|
|
67
|
+
</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
<Text>{formatDate(document.updateTime)}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
|
|
72
|
+
{document.customMetadata && document.customMetadata.length > 0 && (
|
|
73
|
+
<>
|
|
74
|
+
<Box marginTop={1}>
|
|
75
|
+
<Text bold color="yellow">
|
|
76
|
+
Custom Metadata:
|
|
77
|
+
</Text>
|
|
78
|
+
</Box>
|
|
79
|
+
{document.customMetadata.map((meta) => (
|
|
80
|
+
<Box key={meta.key}>
|
|
81
|
+
<Box width={14}>
|
|
82
|
+
<Text dimColor>{meta.key}</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
<Text>{meta.stringValue ?? meta.numericValue ?? "-"}</Text>
|
|
85
|
+
</Box>
|
|
86
|
+
))}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
<Box marginTop={1}>
|
|
90
|
+
<Text dimColor>d: Delete Esc/b: Back q: Quit</Text>
|
|
91
|
+
</Box>
|
|
92
|
+
</Box>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { Document } from "../lib/api.ts";
|
|
3
|
+
import { formatBytes, formatDate, formatState } from "../utils/formatters.ts";
|
|
4
|
+
|
|
5
|
+
interface DocumentListProps {
|
|
6
|
+
documents: Document[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Document一覧テーブル
|
|
13
|
+
export function DocumentList({
|
|
14
|
+
documents,
|
|
15
|
+
selectedIndex,
|
|
16
|
+
loading,
|
|
17
|
+
error,
|
|
18
|
+
}: DocumentListProps) {
|
|
19
|
+
if (loading) {
|
|
20
|
+
return <Text color="gray">Loading...</Text>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
return <Text color="red">Error: {error}</Text>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (documents.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="column">
|
|
30
|
+
<Text color="gray">No documents found</Text>
|
|
31
|
+
<Box marginTop={1}>
|
|
32
|
+
<Text dimColor>u: Upload Esc: Back q: Quit</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box flexDirection="column">
|
|
40
|
+
<Box gap={1} marginBottom={1}>
|
|
41
|
+
<Box width={28}>
|
|
42
|
+
<Text bold dimColor>
|
|
43
|
+
Name
|
|
44
|
+
</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
<Box width={10}>
|
|
47
|
+
<Text bold dimColor>
|
|
48
|
+
State
|
|
49
|
+
</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
<Box width={12}>
|
|
52
|
+
<Text bold dimColor>
|
|
53
|
+
Size
|
|
54
|
+
</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
<Box width={22}>
|
|
57
|
+
<Text bold dimColor>
|
|
58
|
+
Created
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
<Box width={22}>
|
|
62
|
+
<Text bold dimColor>
|
|
63
|
+
Updated
|
|
64
|
+
</Text>
|
|
65
|
+
</Box>
|
|
66
|
+
</Box>
|
|
67
|
+
{documents.map((doc, index) => {
|
|
68
|
+
const state = formatState(doc.state);
|
|
69
|
+
const name = doc.displayName || doc.name.split("/").pop() || "";
|
|
70
|
+
return (
|
|
71
|
+
<Box key={doc.name} gap={1}>
|
|
72
|
+
<Box width={28}>
|
|
73
|
+
<Text color={index === selectedIndex ? "green" : "white"}>
|
|
74
|
+
{index === selectedIndex ? "❯ " : " "}
|
|
75
|
+
{name.slice(0, 24)}
|
|
76
|
+
</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
<Box width={10}>
|
|
79
|
+
<Text color={state.color}>{state.text}</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
<Box width={12}>
|
|
82
|
+
<Text dimColor>{formatBytes(doc.sizeBytes)}</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
<Box width={22}>
|
|
85
|
+
<Text dimColor>{formatDate(doc.createTime)}</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
<Box width={22}>
|
|
88
|
+
<Text dimColor>{formatDate(doc.updateTime)}</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
</Box>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
<Box marginTop={1}>
|
|
94
|
+
<Text dimColor>Up/Down: Select Enter/i: Info u: Upload Esc: Back q: Quit</Text>
|
|
95
|
+
</Box>
|
|
96
|
+
</Box>
|
|
97
|
+
);
|
|
98
|
+
}
|