bigwolfe commited on
Commit
c5b6c1c
·
1 Parent(s): c8b68ff

Update MCP server and widget - getting close to working!

Browse files
Files changed (2) hide show
  1. backend/src/mcp/server.py +32 -21
  2. 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
- print("!!! WIDGET RESOURCE ACCESSED (SMOKE TEST) !!!", flush=True)
 
 
 
49
 
50
- return """<!DOCTYPE html>
51
- <html lang="en">
52
- <head>
53
- <meta charset="UTF-8">
54
- <title>Smoke Test</title>
55
- <style>body { background: #222; color: #0f0; font-family: monospace; padding: 20px; }</style>
56
- </head>
57
- <body>
58
- <h1>Widget Smoke Test</h1>
59
- <p>If you see this, the pipeline is working.</p>
60
- <pre id="output"></pre>
61
- <script>
62
- const output = document.getElementById('output');
63
- output.textContent = 'Window.openai: ' + (window.openai ? 'Present' : 'Missing');
64
- if (window.openai && window.openai.toolOutput) {
65
- output.textContent += '\\nData: ' + JSON.stringify(window.openai.toolOutput, null, 2);
66
- }
67
- </script>
68
- </body>
69
- </html>"""
 
 
 
 
 
 
 
 
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
- // import { NoteViewer } from '@/components/NoteViewer';
5
- // import { SearchWidget } from '@/components/SearchWidget';
6
- // import type { Note } from '@/types/note';
7
- // import type { SearchResult } from '@/types/search';
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 [debugInfo, setDebugInfo] = useState<string>("Initializing...");
 
 
58
 
59
  useEffect(() => {
60
- console.log("Widget mounted");
61
- const toolOutput = window.openai?.toolOutput;
62
- setDebugInfo(`Tool Output: ${JSON.stringify(toolOutput, null, 2)}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }, []);
64
 
 
 
 
 
 
 
 
 
 
 
65
  return (
66
- <div className="dark min-h-screen bg-background text-foreground flex flex-col p-4">
67
- <h1 className="text-2xl font-bold text-green-500">Widget Hello World</h1>
68
- <pre className="bg-muted p-4 rounded mt-4 overflow-auto text-xs font-mono">
69
- {debugInfo}
70
- </pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  };