Skip to main content
Glama
page.tsx17.3 kB
"use client"; import { useEffect, useMemo, useState, useCallback } from "react"; type FileItem = string; interface PathValidation { ok: boolean; reason: string; } export default function KnowledgePage() { const [files, setFiles] = useState<FileItem[]>([]); const [selected, setSelected] = useState<string>(""); const [pathInput, setPathInput] = useState<string>(""); const [content, setContent] = useState<string>(""); const [out, setOut] = useState<string>(""); const [error, setError] = useState<string>(""); const [loading, setLoading] = useState(false); // Modal state const [showNew, setShowNew] = useState(false); const [newPath, setNewPath] = useState<string>("docs/data-learning/new-note.md"); const [creating, setCreating] = useState(false); const [createErr, setCreateErr] = useState<string>(""); const validatePath = useCallback((path: string): PathValidation => { const trimmedPath = path.trim(); if (!trimmedPath) { return { ok: false, reason: "กรุณาระบุ path ของไฟล์" }; } if (!trimmedPath.startsWith("docs/data-learning/")) { return { ok: false, reason: "Path ต้องเริ่มด้วย 'docs/data-learning/'" }; } if (!trimmedPath.toLowerCase().endsWith(".md")) { return { ok: false, reason: "ไฟล์ต้องมีนามสกุล .md" }; } if (/[\\\0\r\n]/.test(trimmedPath) || trimmedPath.includes("..")) { return { ok: false, reason: "พบอักขระที่ไม่อนุญาต" }; } return { ok: true, reason: "✓ Path ถูกต้อง" }; }, []); const pathCheck = useMemo(() => validatePath(newPath), [newPath, validatePath]); const loadFiles = useCallback(async () => { setError(""); try { const res = await fetch(`/api/files`); const data = await res.json(); const list = Array.isArray(data?.files) ? data.files : []; setFiles(list); if (!selected && list.length) setSelected(list[0]); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }, [selected]); const loadFile = useCallback(async (path: string) => { if (!path) return; setError(""); try { const res = await fetch(`/api/file?path=${encodeURIComponent(path)}`); const data = await res.json(); if (data?.content != null) { setPathInput(path); setContent(data.content); } else { setContent(""); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }, []); const saveFile = useCallback(async () => { if (!pathInput) { setError("กรุณาระบุ path ของไฟล์"); return; } setLoading(true); setError(""); setOut(""); try { const res = await fetch(`/api/file`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: pathInput, content }), }); const data = await res.json(); if (res.ok) { setOut(`✓ : ${data?.path}`); await loadFiles(); } else { throw new Error(data?.error || "ไม่สามารถบันทึกได้"); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }, [pathInput, content, loadFiles]); const createFile = useCallback(async () => { const trimmedPath = newPath.trim(); if (!pathCheck.ok) return; setCreateErr(""); setCreating(true); try { const fileName = trimmedPath.split("/").pop() || "New Note"; const initial = `# ${fileName}\n\nสร้างเมื่อ: ${new Date().toLocaleString('th-TH')}\n`; const res = await fetch(`/api/file`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: trimmedPath, content: initial }), }); const data = await res.json(); if (!res.ok || data?.error) { throw new Error(data?.error || `HTTP ${res.status}`); } await loadFiles(); setSelected(trimmedPath); await loadFile(trimmedPath); setShowNew(false); } catch (e) { setCreateErr(e instanceof Error ? e.message : String(e)); } finally { setCreating(false); } }, [newPath, pathCheck.ok, loadFiles, loadFile]); useEffect(() => { loadFiles(); }, [loadFiles]); useEffect(() => { if (selected) loadFile(selected); }, [selected, loadFile]); const fileOptions = useMemo( () => files.map(f => <option key={f} value={f}>{f}</option>), [files] ); return ( <div className="min-h-screen"> {/* Top Navigation Bar */} {/* <div className="bg-white border-b border-gray-200 shadow-sm"> <div className="max-w-[1800px] mx-auto px-6 py-4"> <div className="flex items-center justify-between"> <div> <h1 className="text-xl font-semibold text-gray-900">Knowledge Base</h1> <p className="text-sm text-gray-600 mt-0.5">จัดการเอกสารและความรู้ภายในองค์กร</p> </div> <button onClick={() => { setCreateErr(""); setNewPath("docs/data-learning/new-note.md"); setShowNew(true); }} className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all duration-200 shadow-lg shadow-blue-600/20" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> สร้างไฟล์ใหม่ </button> </div> </div> </div> */} <div className="max-w-[1800px] mx-auto px-6 py-6"> <div className="flex gap-6 h-[calc(100vh-140px)]"> {/* Sidebar - File Explorer */} <div className="w-80 flex-shrink-0"> <div className="bg-white rounded-xl border border-gray-200 h-full flex flex-col overflow-hidden shadow-sm"> <div className="px-4 py-3 border-b border-gray-200 bg-gray-50"> <div className="flex items-center gap-2 text-gray-700"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /> </svg> <span className="font-medium">รายการเอกสาร</span> <button onClick={() => { setCreateErr(""); setNewPath("docs/data-learning/new-note.md"); setShowNew(true); }} className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all duration-200 shadow-lg shadow-blue-600/20" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> </button> </div> </div> <div className="flex-1 overflow-y-auto p-3"> {files.map(file => ( <button key={file} onClick={() => setSelected(file)} className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 text-sm transition-all duration-150 ${ selected === file ? "bg-blue-600 text-white shadow-md" : "text-gray-700 hover:bg-gray-100" }`} > <div className="flex items-center gap-2"> <svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> <span className="truncate">{file.split('/').pop()}</span> </div> <div className={`text-xs mt-1 truncate ${selected === file ? 'opacity-80' : 'opacity-60'}`}>{file}</div> </button> ))} {files.length === 0 && ( <div className="text-center py-8 text-gray-400"> <svg className="w-12 h-12 mx-auto mb-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> <p className="text-sm">ไม่มีเอกสาร</p> </div> )} </div> </div> </div> {/* Main Editor Area */} <div className="flex-1 flex flex-col min-w-0"> <div className="bg-white rounded-xl border border-gray-200 flex-1 flex flex-col overflow-hidden shadow-sm"> {/* Editor Header */} <div className="px-5 py-4 border-b border-gray-200 bg-gray-50"> <div className="flex items-center gap-3"> <svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /> </svg> <input type="text" value={pathInput} onChange={e => setPathInput(e.target.value)} placeholder="docs/data-learning/filename.md" className="flex-1 bg-white border border-gray-300 rounded-lg px-4 py-2 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> <button onClick={saveFile} disabled={loading} className="flex items-center gap-2 px-5 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-all duration-200 shadow-md" > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> {loading ? "Saving..." : "Save"} </button> </div> </div> {/* Editor Content */} <div className="flex-1 p-5 overflow-hidden"> <textarea value={content} onChange={e => setContent(e.target.value)} placeholder="เริ่มเขียนเอกสารของคุณที่นี่..." className="w-full h-full bg-gray-50 border border-gray-300 rounded-lg px-4 py-3 text-gray-900 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none placeholder-gray-400" /> </div> {/* Status Bar */} {(error || out) && ( <div className="px-5 py-3 border-t border-gray-200 bg-gray-50"> {error && ( <div className="flex items-center gap-2 text-red-600 text-sm"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> {error} </div> )} {out && ( <div className="flex items-center gap-2 text-green-600 text-sm"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> {out} </div> )} </div> )} </div> </div> </div> </div> {/* Create File Modal */} {showNew && ( <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => !creating && setShowNew(false)} /> <div className="relative z-10 w-full max-w-md bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden" onKeyDown={e => { if (e.key === "Escape" && !creating) setShowNew(false); }} > <div className="px-6 py-5 border-b border-gray-200 bg-gray-50"> <h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> สร้างไฟล์ใหม่ </h3> <p className="mt-1 text-sm text-gray-600"> เพิ่มเอกสาร Markdown ใหม่ใน docs/data-learning/ </p> </div> <div className="px-6 py-5"> <label className="block text-sm font-medium text-gray-700 mb-2"> Path ของไฟล์ </label> <input type="text" className={`w-full bg-white border rounded-lg px-4 py-2.5 text-sm text-gray-900 outline-none focus:ring-2 transition-all ${ createErr || !pathCheck.ok ? "border-red-400 focus:ring-red-500" : "border-gray-300 focus:ring-blue-500" }`} value={newPath} onChange={e => setNewPath(e.target.value)} placeholder="docs/data-learning/new-note.md" autoFocus /> {createErr ? ( <p className="mt-3 text-sm text-red-600 bg-red-50 px-3 py-2 rounded-lg border border-red-200 flex items-start gap-2"> <svg className="w-4 h-4 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> {createErr} </p> ) : ( <p className={`mt-3 text-sm px-3 py-2 rounded-lg flex items-start gap-2 ${ pathCheck.ok ? "text-green-700 bg-green-50 border border-green-200" : "text-gray-600 bg-gray-50 border border-gray-200" }`} > <svg className="w-4 h-4 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> {pathCheck.reason} </p> )} </div> <div className="px-6 py-4 bg-gray-50 flex items-center justify-end gap-3"> <button onClick={() => setShowNew(false)} disabled={creating} className="px-4 py-2 text-sm font-medium border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-all" > ยกเลิก </button> <button onClick={createFile} disabled={creating || !pathCheck.ok} className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-md" > {creating ? "กำลังสร้าง..." : "สร้างไฟล์"} </button> </div> </div> </div> )} </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tndfame/mcp_management'

If you have feedback or need assistance with the MCP directory API, please join our Discord server