Spaces:
Sleeping
Sleeping
bigwolfe
commited on
Commit
·
c5b6c1c
1
Parent(s):
c8b68ff
Update MCP server and widget - getting close to working!
Browse files- backend/src/mcp/server.py +32 -21
- frontend/src/widget.tsx +93 -14
backend/src/mcp/server.py
CHANGED
|
@@ -45,28 +45,39 @@ auth_service = AuthService()
|
|
| 45 |
@mcp.resource("ui://widget/note.html", mime_type="text/html+skybridge")
|
| 46 |
def widget_resource() -> str:
|
| 47 |
"""Return the widget HTML bundle."""
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
def _current_user_id() -> str:
|
|
|
|
| 45 |
@mcp.resource("ui://widget/note.html", mime_type="text/html+skybridge")
|
| 46 |
def widget_resource() -> str:
|
| 47 |
"""Return the widget HTML bundle."""
|
| 48 |
+
# Locate widget.html relative to project root
|
| 49 |
+
# In Docker: /app/frontend/dist/widget.html
|
| 50 |
+
# Local: frontend/dist/widget.html
|
| 51 |
+
# We use PROJECT_ROOT from config
|
| 52 |
|
| 53 |
+
widget_path = PROJECT_ROOT / "frontend" / "dist" / "widget.html"
|
| 54 |
+
|
| 55 |
+
logger.info(f"Reading widget from: {widget_path}")
|
| 56 |
+
|
| 57 |
+
if not widget_path.exists():
|
| 58 |
+
logger.error(f"Widget path does not exist: {widget_path}")
|
| 59 |
+
return "Widget build not found. Please run 'npm run build' in frontend directory."
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
html_content = widget_path.read_text(encoding="utf-8")
|
| 63 |
+
logger.info(f"Widget content length: {len(html_content)}")
|
| 64 |
+
if not html_content.strip():
|
| 65 |
+
logger.error("Widget file is empty!")
|
| 66 |
+
return "Widget build file is empty."
|
| 67 |
+
|
| 68 |
+
# Replace relative asset paths with absolute URLs for ChatGPT iframe
|
| 69 |
+
config = get_config()
|
| 70 |
+
base_url = config.hf_space_url.rstrip("/")
|
| 71 |
+
logger.info(f"Injecting base URL: {base_url}")
|
| 72 |
+
|
| 73 |
+
# Vite builds usually output /assets/...
|
| 74 |
+
html_content = html_content.replace('src="/assets/', f'src="{base_url}/assets/')
|
| 75 |
+
html_content = html_content.replace('href="/assets/', f'href="{base_url}/assets/')
|
| 76 |
+
|
| 77 |
+
return html_content
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.exception(f"Failed to read widget file: {e}")
|
| 80 |
+
return f"Server error reading widget: {e}"
|
| 81 |
|
| 82 |
|
| 83 |
def _current_user_id() -> str:
|
frontend/src/widget.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
import React, { useEffect, useState, Component, type ErrorInfo } from 'react';
|
| 2 |
import ReactDOM from 'react-dom/client';
|
| 3 |
import './index.css';
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
import { AlertTriangle } from 'lucide-react';
|
| 9 |
|
| 10 |
// Mock window.openai for development
|
| 11 |
if (!window.openai) {
|
|
@@ -54,20 +54,99 @@ class WidgetErrorBoundary extends Component<{ children: React.ReactNode }, { has
|
|
| 54 |
}
|
| 55 |
|
| 56 |
const WidgetApp = () => {
|
| 57 |
-
const [
|
|
|
|
|
|
|
| 58 |
|
| 59 |
useEffect(() => {
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}, []);
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
return (
|
| 66 |
-
<div className="dark min-h-screen bg-background text-foreground flex flex-col
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
);
|
| 73 |
};
|
|
|
|
| 1 |
import React, { useEffect, useState, Component, type ErrorInfo } from 'react';
|
| 2 |
import ReactDOM from 'react-dom/client';
|
| 3 |
import './index.css';
|
| 4 |
+
import { NoteViewer } from '@/components/NoteViewer';
|
| 5 |
+
import { SearchWidget } from '@/components/SearchWidget';
|
| 6 |
+
import type { Note } from '@/types/note';
|
| 7 |
+
import type { SearchResult } => '@/types/search';
|
| 8 |
+
import { Loader2, AlertTriangle } from 'lucide-react';
|
| 9 |
|
| 10 |
// Mock window.openai for development
|
| 11 |
if (!window.openai) {
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
const WidgetApp = () => {
|
| 57 |
+
const [view, setView] = useState<'loading' | 'note' | 'search' | 'error'>('loading');
|
| 58 |
+
const [data, setData] = useState<any>(null);
|
| 59 |
+
const [error, setError] = useState<string | null>(null);
|
| 60 |
|
| 61 |
useEffect(() => {
|
| 62 |
+
// In a real ChatGPT app, toolOutput is injected before script execution or available on load.
|
| 63 |
+
// We check it here.
|
| 64 |
+
const toolOutput = window.openai.toolOutput;
|
| 65 |
+
console.log("Widget loaded with toolOutput:", toolOutput);
|
| 66 |
+
|
| 67 |
+
if (!toolOutput) {
|
| 68 |
+
// Fallback for dev testing via URL
|
| 69 |
+
const params = new URLSearchParams(window.location.search);
|
| 70 |
+
const mockType = params.get('type');
|
| 71 |
+
if (mockType === 'note') {
|
| 72 |
+
// Mock note data
|
| 73 |
+
setView('note');
|
| 74 |
+
setData({
|
| 75 |
+
title: "Demo Note",
|
| 76 |
+
note_path: "demo.md",
|
| 77 |
+
body: "# Demo Note\n\nThis is a **markdown** note rendered in the widget.",
|
| 78 |
+
version: 1,
|
| 79 |
+
size_bytes: 100,
|
| 80 |
+
created: new Date().toISOString(),
|
| 81 |
+
updated: new Date().toISOString(),
|
| 82 |
+
metadata: {}
|
| 83 |
+
});
|
| 84 |
+
} else if (mockType === 'search') {
|
| 85 |
+
setView('search');
|
| 86 |
+
setData([
|
| 87 |
+
{ title: "Result 1", note_path: "res1.md", snippet: "Found match...", score: 1.0, updated: new Date() }
|
| 88 |
+
]);
|
| 89 |
+
} else {
|
| 90 |
+
setError("No content data found. (window.openai.toolOutput is empty)");
|
| 91 |
+
setView('error');
|
| 92 |
+
}
|
| 93 |
+
return;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Detect content type based on shape
|
| 97 |
+
if (toolOutput.note) {
|
| 98 |
+
setView('note');
|
| 99 |
+
setData(toolOutput.note);
|
| 100 |
+
} else if (toolOutput.results) {
|
| 101 |
+
setView('search');
|
| 102 |
+
setData(toolOutput.results);
|
| 103 |
+
} else {
|
| 104 |
+
// Fallback: try to guess or show raw
|
| 105 |
+
console.warn("Unknown tool output format", toolOutput);
|
| 106 |
+
setError("Unknown content format.");
|
| 107 |
+
setView('error');
|
| 108 |
+
}
|
| 109 |
}, []);
|
| 110 |
|
| 111 |
+
const handleWikilinkClick = (linkText: string) => {
|
| 112 |
+
console.log("Clicked wikilink in widget:", linkText);
|
| 113 |
+
alert(`Navigation to "${linkText}" requested. (Widget navigation pending implementation)`);
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const handleNoteSelect = (path: string) => {
|
| 117 |
+
console.log("Selected note from search:", path);
|
| 118 |
+
alert(`Opening "${path}"... (Widget navigation pending implementation)`);
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
return (
|
| 122 |
+
<div className="dark min-h-screen bg-background text-foreground flex flex-col">
|
| 123 |
+
{view === 'loading' && (
|
| 124 |
+
<div className="flex-1 flex items-center justify-center">
|
| 125 |
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
| 126 |
+
</div>
|
| 127 |
+
)}
|
| 128 |
+
|
| 129 |
+
{view === 'error' && (
|
| 130 |
+
<div className="p-4 text-destructive">
|
| 131 |
+
<h2 className="font-bold">Error</h2>
|
| 132 |
+
<p>{error}</p>
|
| 133 |
+
</div>
|
| 134 |
+
)}
|
| 135 |
+
|
| 136 |
+
{view === 'note' && data && (
|
| 137 |
+
<NoteViewer
|
| 138 |
+
note={data as Note}
|
| 139 |
+
backlinks={[]} // Backlinks usually fetched separately, omit for V1 widget
|
| 140 |
+
onWikilinkClick={handleWikilinkClick}
|
| 141 |
+
/>
|
| 142 |
+
)}
|
| 143 |
+
|
| 144 |
+
{view === 'search' && data && (
|
| 145 |
+
<SearchWidget
|
| 146 |
+
results={data as SearchResult[]}
|
| 147 |
+
onSelectNote={handleNoteSelect}
|
| 148 |
+
/>
|
| 149 |
+
)}
|
| 150 |
</div>
|
| 151 |
);
|
| 152 |
};
|