update ui
This commit is contained in:
parent
924e78d387
commit
81ff693bf7
16
README.md
16
README.md
@ -57,9 +57,23 @@ function MyComponent() {
|
||||
|
||||
**`createUI(name, entryUrl)`** - Creates a UI component
|
||||
- `name`: Component identifier
|
||||
- `entryUrl`: Path from `import.meta.resolve()`
|
||||
- `entryUrl`: Path to the component entry file
|
||||
- Returns: `{ component(opts?) }` where opts: `{ props?, frameSize? }`
|
||||
|
||||
The `entryUrl` parameter accepts both formats:
|
||||
|
||||
```typescript
|
||||
// ESM (recommended) - using import.meta.resolve()
|
||||
// Requires "type": "module" in package.json
|
||||
createUI('dashboard', import.meta.resolve('./MyComponent.tsx'));
|
||||
|
||||
// CommonJS - using require.resolve() or absolute paths
|
||||
createUI('dashboard', require.resolve('./MyComponent.tsx'));
|
||||
createUI('dashboard', path.join(__dirname, './MyComponent.tsx'));
|
||||
```
|
||||
|
||||
The library automatically converts `file://` URLs to file paths, so both approaches work seamlessly.
|
||||
|
||||
### UI (`@mcp-ui/library/ui`)
|
||||
|
||||
- **`useProps(defaults)`** - Get props passed from the server
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { generateMockStockData } from './stock-utils';
|
||||
import { callTool, sendPrompt, useProps } from '../../library/ui';
|
||||
import { callTool, sendPrompt, useProps } from '@mcp-ui/library/ui';
|
||||
|
||||
|
||||
// Types for props passed from the tool handler
|
||||
@ -74,7 +74,7 @@ Please analyze this portfolio and provide recommendations.`;
|
||||
{/* Header */}
|
||||
<div className="header">
|
||||
<div>
|
||||
<h1>📈 Stock Portfolio!!</h1>
|
||||
<h1>📈 Stock Portfolio</h1>
|
||||
<p className="subtitle">Timeframe: {props.timeframe} • {stocks.length} stocks</p>
|
||||
</div>
|
||||
<div className="portfolio-summary">
|
||||
@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MCP UI Inspector</title>
|
||||
</head>
|
||||
|
||||
@ -17,12 +17,13 @@
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--text-primary);
|
||||
gap: 7px;
|
||||
color: var(--white);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-brand svg {
|
||||
color: var(--accent-purple);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.header-brand h1 {
|
||||
@ -67,8 +68,13 @@
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main layout */
|
||||
|
||||
@ -4,6 +4,7 @@ import { ToolsPanel } from './components/ToolsPanel'
|
||||
import { ResultsPane } from './components/ResultsPane'
|
||||
import { useMCP } from './hooks/useMCP'
|
||||
import './App.css'
|
||||
import { Eye, Sparkles } from 'lucide-react'
|
||||
|
||||
export type Tool = {
|
||||
name: string
|
||||
@ -43,6 +44,7 @@ function App() {
|
||||
const [selectedTool, setSelectedTool] = useState<Tool | null>(null)
|
||||
const [toolResult, setToolResult] = useState<ToolResult | null>(null)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null)
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
if (isConnected) {
|
||||
@ -57,6 +59,7 @@ function App() {
|
||||
const handleExecuteTool = useCallback(async (toolName: string, params: Record<string, unknown>) => {
|
||||
setIsExecuting(true)
|
||||
setToolResult(null)
|
||||
setLastExecution({ toolName, params })
|
||||
|
||||
try {
|
||||
const result = await callTool(toolName, params)
|
||||
@ -76,15 +79,16 @@ function App() {
|
||||
}
|
||||
}, [callTool])
|
||||
|
||||
const handleReload = useCallback(async () => {
|
||||
if (lastExecution) {
|
||||
await handleExecuteTool(lastExecution.toolName, lastExecution.params)
|
||||
}
|
||||
}, [lastExecution, handleExecuteTool])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<div className="header-brand">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<div className="header-brand" onClick={() => window.location.reload()}>
|
||||
<h1>MCP UI Inspector</h1>
|
||||
</div>
|
||||
<div className="header-status">
|
||||
@ -127,6 +131,7 @@ function App() {
|
||||
<ResultsPane
|
||||
result={toolResult}
|
||||
isExecuting={isExecuting}
|
||||
onReload={lastExecution ? handleReload : undefined}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -9,16 +9,17 @@
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-black);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
outline: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@ -28,14 +29,8 @@
|
||||
|
||||
/* Primary Variant */
|
||||
.btn-primary {
|
||||
background: var(--accent-blue);
|
||||
border-color: var(--accent-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #4c9aed;
|
||||
border-color: #4c9aed;
|
||||
background: var(--white);
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Ghost Variant (icon buttons) */
|
||||
@ -45,11 +40,6 @@
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn-loading {
|
||||
cursor: wait;
|
||||
|
||||
@ -49,13 +49,21 @@
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--accent-blue);
|
||||
color: var(--white);
|
||||
border-bottom-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
font-size: 10px;
|
||||
color: var(--accent-green);
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-left: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.tab-badge.active {
|
||||
background: var(--accent-blue);
|
||||
}
|
||||
|
||||
.results-actions {
|
||||
@ -152,7 +160,9 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-frame {
|
||||
@ -169,7 +179,8 @@
|
||||
}
|
||||
|
||||
.text-output pre {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||
monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2, RotateCw } from 'lucide-react'
|
||||
import { Button } from './Button'
|
||||
import type { ToolResult } from '../App'
|
||||
import './ResultsPane.css'
|
||||
@ -7,37 +7,46 @@ import './ResultsPane.css'
|
||||
interface ResultsPaneProps {
|
||||
result: ToolResult | null
|
||||
isExecuting: boolean
|
||||
onReload?: () => void
|
||||
}
|
||||
|
||||
type Tab = 'ui' | 'text'
|
||||
|
||||
export function ResultsPane({ result, isExecuting }: ResultsPaneProps) {
|
||||
export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('ui')
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
const hasUI = result?.htmlContent != null
|
||||
const hasText = result?.textContent != null
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUI) {
|
||||
setActiveTab('ui')
|
||||
} else if (hasText) {
|
||||
setActiveTab('text')
|
||||
}
|
||||
}, [hasText, hasUI])
|
||||
|
||||
return (
|
||||
<div className={`results-pane ${isFullscreen ? 'fullscreen' : ''}`}>
|
||||
<div className="panel-header">
|
||||
<div className="results-tabs">
|
||||
<Button
|
||||
<button
|
||||
className={`tab ${activeTab === 'ui' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('ui')}
|
||||
icon={<Monitor size={14} />}
|
||||
>
|
||||
UI Output
|
||||
{hasUI && <span className="tab-badge">✓</span>}
|
||||
</Button>
|
||||
<Button
|
||||
<Monitor size={14} />
|
||||
UI
|
||||
<span className={`tab-badge ${hasUI ? 'active' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === 'text' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('text')}
|
||||
icon={<FileText size={14} />}
|
||||
>
|
||||
Text Response
|
||||
{hasText && <span className="tab-badge">✓</span>}
|
||||
</Button>
|
||||
<FileText size={14} />
|
||||
Text
|
||||
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="results-actions">
|
||||
@ -47,6 +56,15 @@ export function ResultsPane({ result, isExecuting }: ResultsPaneProps) {
|
||||
{result.timestamp.toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
{onReload && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onReload}
|
||||
disabled={isExecuting}
|
||||
title="Reload"
|
||||
icon={<RotateCw size={14} />}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
|
||||
@ -21,13 +21,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
padding: 13px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-section-content {
|
||||
@ -99,7 +98,9 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-about {
|
||||
|
||||
@ -62,7 +62,7 @@ export function Sidebar({
|
||||
{isConnected && sessionId && (
|
||||
<div className="session-info">
|
||||
<div className="session-label">Session ID</div>
|
||||
<code className="session-id">{sessionId.slice(0, 8)}...</code>
|
||||
<code className="session-id">{sessionId.slice(0, 26)}...</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
.tool-count {
|
||||
background: var(--bg-hover);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
@ -165,7 +165,8 @@
|
||||
|
||||
.json-editor {
|
||||
width: 100%;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
@ -240,7 +241,9 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Wrench, Play, ChevronRight, Code2, FileJson } from 'lucide-react'
|
||||
import { Wrench, Play, Circle, CircleDot, Code2, FileJson } from 'lucide-react'
|
||||
import { Button } from './Button'
|
||||
import type { Tool } from '../App'
|
||||
import './ToolsPanel.css'
|
||||
@ -148,7 +148,7 @@ export function ToolsPanel({
|
||||
className={`tool-item ${selectedTool?.name === tool.name ? 'selected' : ''}`}
|
||||
onClick={() => onSelectTool(tool)}
|
||||
>
|
||||
<ChevronRight size={14} className="tool-chevron" />
|
||||
{selectedTool?.name === tool.name ? <CircleDot size={14} className="tool-chevron" /> : <Circle size={14} className="tool-chevron" />}
|
||||
<div className="tool-info">
|
||||
<span className="tool-name">{tool.name}</span>
|
||||
{tool.description && (
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
--bg-primary: #000000;
|
||||
--bg-secondary: #000000;
|
||||
--bg-tertiary: #141414;
|
||||
--bg-hover: #30363d;
|
||||
--border-color: #30363d;
|
||||
--text-primary: #e6edf3;
|
||||
--text-secondary: #8b949e;
|
||||
@ -18,6 +17,7 @@
|
||||
--accent-red: #f85149;
|
||||
--accent-orange: #d29922;
|
||||
--accent-purple: #a371f7;
|
||||
--white: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -69,51 +69,7 @@ textarea {
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
border-color: var(--accent-blue);
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
button {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--accent-blue);
|
||||
border-color: var(--accent-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: #4c9aed;
|
||||
}
|
||||
|
||||
button.success {
|
||||
background: var(--accent-green);
|
||||
border-color: var(--accent-green);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.success:hover {
|
||||
background: #2ea043;
|
||||
border-color: var(--white);
|
||||
}
|
||||
|
||||
/* Code/mono text */
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
|
||||
// Cache bundled JS per entry path
|
||||
// Cache bundled JS per entry path (only used in production)
|
||||
const bundleCache = new Map<string, string>();
|
||||
|
||||
// In development mode, skip cache to allow hot-reloading of component changes
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export async function bundleComponent(entryPath: string): Promise<string> {
|
||||
if (bundleCache.has(entryPath)) {
|
||||
if (!isDev && bundleCache.has(entryPath)) {
|
||||
return bundleCache.get(entryPath)!;
|
||||
}
|
||||
|
||||
@ -23,6 +26,11 @@ export async function bundleComponent(entryPath: string): Promise<string> {
|
||||
});
|
||||
|
||||
const bundledJS = result.outputFiles[0].text;
|
||||
bundleCache.set(entryPath, bundledJS);
|
||||
|
||||
// Only cache in production mode
|
||||
if (!isDev) {
|
||||
bundleCache.set(entryPath, bundledJS);
|
||||
}
|
||||
|
||||
return bundledJS;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user