@tleblancureta/proto 0.1.0
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/core-web/src/ProtoApp.tsx +163 -0
- package/core-web/src/components/Shell.tsx +276 -0
- package/core-web/src/components/shell/EmptyState.tsx +33 -0
- package/core-web/src/components/shell/FocusView.tsx +55 -0
- package/core-web/src/components/shell/Toolbar.tsx +233 -0
- package/core-web/src/components/shell/persistence.ts +20 -0
- package/core-web/src/components/shell/types.ts +14 -0
- package/core-web/src/components/ui/avatar.tsx +18 -0
- package/core-web/src/components/ui/badge.tsx +28 -0
- package/core-web/src/components/ui/button.tsx +40 -0
- package/core-web/src/components/ui/card.tsx +32 -0
- package/core-web/src/components/ui/inline-edit.tsx +120 -0
- package/core-web/src/components/ui/input.tsx +18 -0
- package/core-web/src/components/ui/scroll-area.tsx +12 -0
- package/core-web/src/components/ui/separator.tsx +23 -0
- package/core-web/src/components/ui/shell-dialog.tsx +79 -0
- package/core-web/src/components/ui/skeleton.tsx +9 -0
- package/core-web/src/components/ui/textarea.tsx +17 -0
- package/core-web/src/components/widgets/agent/Generative.tsx +74 -0
- package/core-web/src/components/widgets/agent/Primitives.tsx +225 -0
- package/core-web/src/components/widgets/agent/actions.ts +52 -0
- package/core-web/src/hooks/useAuth.ts +80 -0
- package/core-web/src/hooks/useData.ts +44 -0
- package/core-web/src/hooks/useMountEffect.ts +10 -0
- package/core-web/src/hooks/useTheme.ts +37 -0
- package/core-web/src/index.ts +52 -0
- package/core-web/src/lib/api.ts +231 -0
- package/core-web/src/lib/config.ts +14 -0
- package/core-web/src/lib/define-widget.ts +71 -0
- package/core-web/src/lib/drag.ts +45 -0
- package/core-web/src/lib/supabase.ts +6 -0
- package/core-web/src/lib/utils.ts +6 -0
- package/core-web/src/lib/widgetCache.ts +29 -0
- package/core-web/src/vite-env.d.ts +1 -0
- package/dist/core-mcp/src/app.d.ts +40 -0
- package/dist/core-mcp/src/app.d.ts.map +1 -0
- package/dist/core-mcp/src/app.js +141 -0
- package/dist/core-mcp/src/app.js.map +1 -0
- package/dist/core-mcp/src/define-tool.d.ts +70 -0
- package/dist/core-mcp/src/define-tool.d.ts.map +1 -0
- package/dist/core-mcp/src/define-tool.js +38 -0
- package/dist/core-mcp/src/define-tool.js.map +1 -0
- package/dist/core-mcp/src/entity-tools.d.ts +27 -0
- package/dist/core-mcp/src/entity-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/entity-tools.js +99 -0
- package/dist/core-mcp/src/entity-tools.js.map +1 -0
- package/dist/core-mcp/src/index.d.ts +36 -0
- package/dist/core-mcp/src/index.d.ts.map +1 -0
- package/dist/core-mcp/src/index.js +116 -0
- package/dist/core-mcp/src/index.js.map +1 -0
- package/dist/core-mcp/src/supabase.d.ts +7 -0
- package/dist/core-mcp/src/supabase.d.ts.map +1 -0
- package/dist/core-mcp/src/supabase.js +18 -0
- package/dist/core-mcp/src/supabase.js.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts +44 -0
- package/dist/core-mcp/src/tools/_helpers.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/_helpers.js +23 -0
- package/dist/core-mcp/src/tools/_helpers.js.map +1 -0
- package/dist/core-mcp/src/tools/ui.d.ts +9 -0
- package/dist/core-mcp/src/tools/ui.d.ts.map +1 -0
- package/dist/core-mcp/src/tools/ui.js +100 -0
- package/dist/core-mcp/src/tools/ui.js.map +1 -0
- package/dist/core-mcp/src/workflow-tools.d.ts +41 -0
- package/dist/core-mcp/src/workflow-tools.d.ts.map +1 -0
- package/dist/core-mcp/src/workflow-tools.js +382 -0
- package/dist/core-mcp/src/workflow-tools.js.map +1 -0
- package/dist/core-shared/src/define-entity.d.ts +73 -0
- package/dist/core-shared/src/define-entity.d.ts.map +1 -0
- package/dist/core-shared/src/define-entity.js +47 -0
- package/dist/core-shared/src/define-entity.js.map +1 -0
- package/dist/core-shared/src/define-workflow.d.ts +111 -0
- package/dist/core-shared/src/define-workflow.d.ts.map +1 -0
- package/dist/core-shared/src/define-workflow.js +92 -0
- package/dist/core-shared/src/define-workflow.js.map +1 -0
- package/dist/core-shared/src/index.d.ts +5 -0
- package/dist/core-shared/src/index.d.ts.map +1 -0
- package/dist/core-shared/src/index.js +7 -0
- package/dist/core-shared/src/index.js.map +1 -0
- package/dist/core-shared/src/scheduling.d.ts +69 -0
- package/dist/core-shared/src/scheduling.d.ts.map +1 -0
- package/dist/core-shared/src/scheduling.js +39 -0
- package/dist/core-shared/src/scheduling.js.map +1 -0
- package/dist/core-shared/src/schemas.d.ts +51 -0
- package/dist/core-shared/src/schemas.d.ts.map +1 -0
- package/dist/core-shared/src/schemas.js +18 -0
- package/dist/core-shared/src/schemas.js.map +1 -0
- package/dist/core-web/src/ProtoApp.d.ts +19 -0
- package/dist/core-web/src/ProtoApp.d.ts.map +1 -0
- package/dist/core-web/src/ProtoApp.js +92 -0
- package/dist/core-web/src/ProtoApp.js.map +1 -0
- package/dist/core-web/src/components/Shell.d.ts +46 -0
- package/dist/core-web/src/components/Shell.d.ts.map +1 -0
- package/dist/core-web/src/components/Shell.js +104 -0
- package/dist/core-web/src/components/Shell.js.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts +13 -0
- package/dist/core-web/src/components/shell/EmptyState.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/EmptyState.js +7 -0
- package/dist/core-web/src/components/shell/EmptyState.js.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts +16 -0
- package/dist/core-web/src/components/shell/FocusView.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/FocusView.js +12 -0
- package/dist/core-web/src/components/shell/FocusView.js.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts +35 -0
- package/dist/core-web/src/components/shell/Toolbar.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/Toolbar.js +42 -0
- package/dist/core-web/src/components/shell/Toolbar.js.map +1 -0
- package/dist/core-web/src/components/shell/persistence.d.ts +8 -0
- package/dist/core-web/src/components/shell/persistence.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/persistence.js +20 -0
- package/dist/core-web/src/components/shell/persistence.js.map +1 -0
- package/dist/core-web/src/components/shell/types.d.ts +13 -0
- package/dist/core-web/src/components/shell/types.d.ts.map +1 -0
- package/dist/core-web/src/components/shell/types.js +2 -0
- package/dist/core-web/src/components/shell/types.js.map +1 -0
- package/dist/core-web/src/components/ui/avatar.d.ts +5 -0
- package/dist/core-web/src/components/ui/avatar.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/avatar.js +9 -0
- package/dist/core-web/src/components/ui/avatar.js.map +1 -0
- package/dist/core-web/src/components/ui/badge.d.ts +13 -0
- package/dist/core-web/src/components/ui/badge.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/badge.js +13 -0
- package/dist/core-web/src/components/ui/badge.js.map +1 -0
- package/dist/core-web/src/components/ui/button.d.ts +22 -0
- package/dist/core-web/src/components/ui/button.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/button.js +21 -0
- package/dist/core-web/src/components/ui/button.js.map +1 -0
- package/dist/core-web/src/components/ui/card.d.ts +7 -0
- package/dist/core-web/src/components/ui/card.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/card.js +13 -0
- package/dist/core-web/src/components/ui/card.js.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts +20 -0
- package/dist/core-web/src/components/ui/inline-edit.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/inline-edit.js +63 -0
- package/dist/core-web/src/components/ui/inline-edit.js.map +1 -0
- package/dist/core-web/src/components/ui/input.d.ts +4 -0
- package/dist/core-web/src/components/ui/input.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/input.js +7 -0
- package/dist/core-web/src/components/ui/input.js.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts +4 -0
- package/dist/core-web/src/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/scroll-area.js +7 -0
- package/dist/core-web/src/components/ui/scroll-area.js.map +1 -0
- package/dist/core-web/src/components/ui/separator.d.ts +7 -0
- package/dist/core-web/src/components/ui/separator.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/separator.js +7 -0
- package/dist/core-web/src/components/ui/separator.js.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts +16 -0
- package/dist/core-web/src/components/ui/shell-dialog.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/shell-dialog.js +36 -0
- package/dist/core-web/src/components/ui/shell-dialog.js.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts +3 -0
- package/dist/core-web/src/components/ui/skeleton.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/skeleton.js +7 -0
- package/dist/core-web/src/components/ui/skeleton.js.map +1 -0
- package/dist/core-web/src/components/ui/textarea.d.ts +4 -0
- package/dist/core-web/src/components/ui/textarea.d.ts.map +1 -0
- package/dist/core-web/src/components/ui/textarea.js +7 -0
- package/dist/core-web/src/components/ui/textarea.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts +13 -0
- package/dist/core-web/src/components/widgets/agent/Generative.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js +42 -0
- package/dist/core-web/src/components/widgets/agent/Generative.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts +79 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js +116 -0
- package/dist/core-web/src/components/widgets/agent/Primitives.js.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts +3 -0
- package/dist/core-web/src/components/widgets/agent/actions.d.ts.map +1 -0
- package/dist/core-web/src/components/widgets/agent/actions.js +33 -0
- package/dist/core-web/src/components/widgets/agent/actions.js.map +1 -0
- package/dist/core-web/src/hooks/useAuth.d.ts +25 -0
- package/dist/core-web/src/hooks/useAuth.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useAuth.js +53 -0
- package/dist/core-web/src/hooks/useAuth.js.map +1 -0
- package/dist/core-web/src/hooks/useData.d.ts +10 -0
- package/dist/core-web/src/hooks/useData.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useData.js +37 -0
- package/dist/core-web/src/hooks/useData.js.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts +6 -0
- package/dist/core-web/src/hooks/useMountEffect.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useMountEffect.js +10 -0
- package/dist/core-web/src/hooks/useMountEffect.js.map +1 -0
- package/dist/core-web/src/hooks/useTheme.d.ts +6 -0
- package/dist/core-web/src/hooks/useTheme.d.ts.map +1 -0
- package/dist/core-web/src/hooks/useTheme.js +31 -0
- package/dist/core-web/src/hooks/useTheme.js.map +1 -0
- package/dist/core-web/src/index.d.ts +33 -0
- package/dist/core-web/src/index.d.ts.map +1 -0
- package/dist/core-web/src/index.js +38 -0
- package/dist/core-web/src/index.js.map +1 -0
- package/dist/core-web/src/lib/api.d.ts +60 -0
- package/dist/core-web/src/lib/api.d.ts.map +1 -0
- package/dist/core-web/src/lib/api.js +204 -0
- package/dist/core-web/src/lib/api.js.map +1 -0
- package/dist/core-web/src/lib/config.d.ts +10 -0
- package/dist/core-web/src/lib/config.d.ts.map +1 -0
- package/dist/core-web/src/lib/config.js +10 -0
- package/dist/core-web/src/lib/config.js.map +1 -0
- package/dist/core-web/src/lib/define-widget.d.ts +52 -0
- package/dist/core-web/src/lib/define-widget.d.ts.map +1 -0
- package/dist/core-web/src/lib/define-widget.js +14 -0
- package/dist/core-web/src/lib/define-widget.js.map +1 -0
- package/dist/core-web/src/lib/drag.d.ts +20 -0
- package/dist/core-web/src/lib/drag.d.ts.map +1 -0
- package/dist/core-web/src/lib/drag.js +33 -0
- package/dist/core-web/src/lib/drag.js.map +1 -0
- package/dist/core-web/src/lib/supabase.d.ts +2 -0
- package/dist/core-web/src/lib/supabase.d.ts.map +1 -0
- package/dist/core-web/src/lib/supabase.js +5 -0
- package/dist/core-web/src/lib/supabase.js.map +1 -0
- package/dist/core-web/src/lib/utils.d.ts +3 -0
- package/dist/core-web/src/lib/utils.d.ts.map +1 -0
- package/dist/core-web/src/lib/utils.js +6 -0
- package/dist/core-web/src/lib/utils.js.map +1 -0
- package/dist/core-web/src/lib/widgetCache.d.ts +18 -0
- package/dist/core-web/src/lib/widgetCache.d.ts.map +1 -0
- package/dist/core-web/src/lib/widgetCache.js +28 -0
- package/dist/core-web/src/lib/widgetCache.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +2 -0
- package/dist/shared.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive renderer for agent-generated UI specs.
|
|
3
|
+
*
|
|
4
|
+
* The agent sends a JSON tree via the `render_ui` MCP tool. Each node has
|
|
5
|
+
* `{ type: 'PrimitiveName', ...props, children?: UINode[] }`. Unknown types
|
|
6
|
+
* render a muted fallback so the agent can iterate.
|
|
7
|
+
*/
|
|
8
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
9
|
+
import * as P from './Primitives'
|
|
10
|
+
|
|
11
|
+
export interface UINode {
|
|
12
|
+
type: string
|
|
13
|
+
children?: UINode[]
|
|
14
|
+
[prop: string]: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PRIMITIVES: Record<string, ComponentType<any>> = {
|
|
18
|
+
Stack: P.Stack,
|
|
19
|
+
Row: P.Row,
|
|
20
|
+
Grid: P.Grid,
|
|
21
|
+
Heading: P.Heading,
|
|
22
|
+
Text: P.Text,
|
|
23
|
+
Image: P.Image,
|
|
24
|
+
Badge: P.Badge,
|
|
25
|
+
Stat: P.Stat,
|
|
26
|
+
Rating: P.Rating,
|
|
27
|
+
GoldSupplier: P.GoldSupplier,
|
|
28
|
+
Card: P.Card,
|
|
29
|
+
CardBody: P.CardBody,
|
|
30
|
+
LinkOut: P.LinkOut,
|
|
31
|
+
Button: P.Button,
|
|
32
|
+
Table: P.Table,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const KNOWN_PRIMITIVES = Object.keys(PRIMITIVES)
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
spec: UINode | UINode[] | null | undefined
|
|
39
|
+
onSendToChat?: (message: string) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Generative({ spec, onSendToChat }: Props) {
|
|
43
|
+
if (!spec) return null
|
|
44
|
+
const nodes = Array.isArray(spec) ? spec : [spec]
|
|
45
|
+
return <>{nodes.map((n, i) => renderNode(n, i, onSendToChat))}</>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderNode(node: UINode, key: number | string, onSendToChat?: (m: string) => void): ReactNode {
|
|
49
|
+
if (!node || typeof node !== 'object') return null
|
|
50
|
+
const { type, children, ...props } = node
|
|
51
|
+
const Cmp = PRIMITIVES[type]
|
|
52
|
+
|
|
53
|
+
if (!Cmp) {
|
|
54
|
+
return (
|
|
55
|
+
<div key={key} className="text-[10px] text-muted-foreground/60 italic">
|
|
56
|
+
[unknown primitive: {String(type)}]
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Inject onSendToChat into any primitive that accepts it (Button)
|
|
62
|
+
const injectedProps = type === 'Button' ? { ...props, onSendToChat } : props
|
|
63
|
+
|
|
64
|
+
const renderedChildren =
|
|
65
|
+
Array.isArray(children) && children.length > 0
|
|
66
|
+
? children.map((c, i) => renderNode(c, `${key}-${i}`, onSendToChat))
|
|
67
|
+
: undefined
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Cmp key={key} {...injectedProps}>
|
|
71
|
+
{renderedChildren}
|
|
72
|
+
</Cmp>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Primitive building blocks for agent-generated UIs.
|
|
3
|
+
*
|
|
4
|
+
* Each primitive is a simple React component that accepts typed props and
|
|
5
|
+
* optional `children` (already rendered by Generative). Primitives have no
|
|
6
|
+
* side effects beyond `onSendToChat` for interactive ones.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, type ReactNode } from 'react'
|
|
9
|
+
import { ExternalLinkIcon, StarIcon, ShieldCheckIcon, Loader2Icon, CheckIcon } from 'lucide-react'
|
|
10
|
+
import { Badge as UIBadge } from '../../ui/badge'
|
|
11
|
+
import { ACTIONS } from './actions'
|
|
12
|
+
|
|
13
|
+
type OnChat = (message: string) => void
|
|
14
|
+
|
|
15
|
+
// ---------- Layout ----------
|
|
16
|
+
|
|
17
|
+
export function Stack({ gap = 2, children }: { gap?: number; children?: ReactNode }) {
|
|
18
|
+
return <div className={`flex flex-col gap-${Math.min(gap, 6)}`}>{children}</div>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Row({ gap = 2, align = 'center', children }: { gap?: number; align?: 'start' | 'center' | 'end' | 'baseline'; children?: ReactNode }) {
|
|
22
|
+
const a = { start: 'items-start', center: 'items-center', end: 'items-end', baseline: 'items-baseline' }[align]
|
|
23
|
+
return <div className={`flex flex-row gap-${Math.min(gap, 6)} ${a}`}>{children}</div>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Grid({ cols = 3, gap = 2, children }: { cols?: 1 | 2 | 3 | 4; gap?: number; children?: ReactNode }) {
|
|
27
|
+
// Default to 3 columns. Tight breakpoints so it actually shows 3 at normal shell widths.
|
|
28
|
+
const c = {
|
|
29
|
+
1: 'grid-cols-1',
|
|
30
|
+
2: 'grid-cols-2',
|
|
31
|
+
3: 'grid-cols-3',
|
|
32
|
+
4: 'grid-cols-4',
|
|
33
|
+
}[cols] || 'grid-cols-4'
|
|
34
|
+
return <div className={`grid ${c} gap-${Math.min(gap, 6)}`}>{children}</div>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------- Typography ----------
|
|
38
|
+
|
|
39
|
+
export function Heading({ text, level = 2 }: { text: string; level?: 1 | 2 | 3 }) {
|
|
40
|
+
const size = level === 1 ? 'text-lg font-semibold' : level === 2 ? 'text-sm font-semibold' : 'text-xs font-medium text-muted-foreground'
|
|
41
|
+
return <p className={size}>{text}</p>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Text({ text, muted, size = 'sm' }: { text: string; muted?: boolean; size?: 'xs' | 'sm' }) {
|
|
45
|
+
return <p className={`${size === 'xs' ? 'text-[11px]' : 'text-xs'} ${muted ? 'text-muted-foreground' : ''}`}>{text}</p>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------- Content ----------
|
|
49
|
+
|
|
50
|
+
export function Image({ src, alt, aspect = 'square', fit = 'contain' }: { src: string; alt?: string; aspect?: 'square' | 'video' | 'auto'; fit?: 'cover' | 'contain' }) {
|
|
51
|
+
const a = aspect === 'square' ? 'aspect-square max-h-32' : aspect === 'video' ? 'aspect-video' : ''
|
|
52
|
+
const f = fit === 'contain' ? 'object-contain' : 'object-cover'
|
|
53
|
+
return (
|
|
54
|
+
<div className={`${a} bg-muted/20 overflow-hidden rounded-md flex items-center justify-center`}>
|
|
55
|
+
<img src={src} alt={alt || ''} loading="lazy" className={`w-full h-full ${f}`} />
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Badge({ text, variant = 'default' }: { text: string; variant?: 'default' | 'secondary' | 'outline' | 'success' | 'warning' }) {
|
|
61
|
+
const colors: Record<string, string> = {
|
|
62
|
+
success: 'bg-emerald-600/15 text-emerald-500 border-emerald-600/30',
|
|
63
|
+
warning: 'bg-amber-500/15 text-amber-500 border-amber-500/30',
|
|
64
|
+
}
|
|
65
|
+
if (colors[variant]) {
|
|
66
|
+
return <span className={`text-[10px] px-1.5 py-0.5 rounded border ${colors[variant]}`}>{text}</span>
|
|
67
|
+
}
|
|
68
|
+
return <UIBadge variant={variant as any} className="text-[10px]">{text}</UIBadge>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function Stat({ label, value, hint, tone = 'default' }: { label: string; value: string; hint?: string; tone?: 'default' | 'success' | 'warning' | 'danger' }) {
|
|
72
|
+
const color = { default: '', success: 'text-emerald-500', warning: 'text-amber-500', danger: 'text-red-500' }[tone]
|
|
73
|
+
return (
|
|
74
|
+
<div className="bg-accent/40 border border-border/60 rounded-lg p-2.5">
|
|
75
|
+
<p className="text-[10px] text-muted-foreground uppercase tracking-wide">{label}</p>
|
|
76
|
+
<p className={`text-base font-semibold ${color}`}>{value}</p>
|
|
77
|
+
{hint && <p className="text-[10px] text-muted-foreground/60 mt-0.5">{hint}</p>}
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function Rating({ score, count }: { score: number; count?: number }) {
|
|
83
|
+
return (
|
|
84
|
+
<span className="inline-flex items-center gap-0.5 text-[11px]">
|
|
85
|
+
<StarIcon className="w-3 h-3 text-yellow-500 fill-yellow-500" />
|
|
86
|
+
{score.toFixed(1)}
|
|
87
|
+
{count ? <span className="text-muted-foreground/60"> ({count})</span> : null}
|
|
88
|
+
</span>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function GoldSupplier({ years }: { years: number }) {
|
|
93
|
+
if (!years) return null
|
|
94
|
+
return (
|
|
95
|
+
<span className="inline-flex items-center gap-0.5 text-[11px] text-amber-500">
|
|
96
|
+
<ShieldCheckIcon className="w-3 h-3" /> {years}y Gold
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------- Containers ----------
|
|
102
|
+
|
|
103
|
+
export function Card({ children }: { children?: ReactNode }) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="bg-accent/40 border border-border/60 rounded-lg overflow-hidden flex flex-col hover:border-primary/40 transition-colors">
|
|
106
|
+
{children}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function CardBody({ children }: { children?: ReactNode }) {
|
|
112
|
+
return <div className="p-2.5 flex flex-col gap-1.5 flex-1">{children}</div>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- Interactive ----------
|
|
116
|
+
|
|
117
|
+
export function LinkOut({ href, label }: { href: string; label?: string }) {
|
|
118
|
+
return (
|
|
119
|
+
<a
|
|
120
|
+
href={href}
|
|
121
|
+
target="_blank"
|
|
122
|
+
rel="noreferrer"
|
|
123
|
+
className="inline-flex items-center gap-1 text-[11px] py-1 px-2 rounded border border-border hover:bg-accent transition-colors"
|
|
124
|
+
>
|
|
125
|
+
{label || 'Ver'} <ExternalLinkIcon className="w-2.5 h-2.5" />
|
|
126
|
+
</a>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function Button({
|
|
131
|
+
label,
|
|
132
|
+
send,
|
|
133
|
+
action,
|
|
134
|
+
actionPayload,
|
|
135
|
+
variant = 'default',
|
|
136
|
+
onSendToChat,
|
|
137
|
+
}: {
|
|
138
|
+
label: string
|
|
139
|
+
send?: string
|
|
140
|
+
action?: string
|
|
141
|
+
actionPayload?: Record<string, any>
|
|
142
|
+
variant?: 'default' | 'primary' | 'ghost'
|
|
143
|
+
onSendToChat?: OnChat
|
|
144
|
+
}) {
|
|
145
|
+
const [state, setState] = useState<'idle' | 'running' | 'done' | 'error'>('idle')
|
|
146
|
+
const [result, setResult] = useState<string | null>(null)
|
|
147
|
+
|
|
148
|
+
const cls =
|
|
149
|
+
state === 'done'
|
|
150
|
+
? 'bg-emerald-600/20 text-emerald-500 border border-emerald-600/30 cursor-default'
|
|
151
|
+
: variant === 'primary'
|
|
152
|
+
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
|
|
153
|
+
: variant === 'ghost'
|
|
154
|
+
? 'hover:bg-accent'
|
|
155
|
+
: 'border border-border hover:bg-accent'
|
|
156
|
+
|
|
157
|
+
async function handle() {
|
|
158
|
+
if (state === 'running' || state === 'done') return
|
|
159
|
+
// Direct action takes priority
|
|
160
|
+
if (action && ACTIONS[action]) {
|
|
161
|
+
setState('running')
|
|
162
|
+
try {
|
|
163
|
+
const msg = await ACTIONS[action](actionPayload || {})
|
|
164
|
+
setResult(msg)
|
|
165
|
+
setState('done')
|
|
166
|
+
} catch (e: any) {
|
|
167
|
+
setResult(`Error: ${e?.message || String(e)}`)
|
|
168
|
+
setState('error')
|
|
169
|
+
setTimeout(() => setState('idle'), 2500)
|
|
170
|
+
}
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
// Fallback: send chat message
|
|
174
|
+
if (send) onSendToChat?.(send)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<button
|
|
179
|
+
onClick={handle}
|
|
180
|
+
disabled={state === 'running' || state === 'done'}
|
|
181
|
+
className={`text-[11px] py-1 px-2 rounded transition-colors inline-flex items-center gap-1 ${cls}`}
|
|
182
|
+
>
|
|
183
|
+
{state === 'running' && <Loader2Icon className="w-2.5 h-2.5 animate-spin" />}
|
|
184
|
+
{state === 'done' && <CheckIcon className="w-2.5 h-2.5" />}
|
|
185
|
+
<span>{state === 'done' ? result || 'Guardado' : state === 'error' ? result || 'Error' : label}</span>
|
|
186
|
+
</button>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------- Tabular ----------
|
|
191
|
+
|
|
192
|
+
export function Table({
|
|
193
|
+
columns,
|
|
194
|
+
rows,
|
|
195
|
+
}: {
|
|
196
|
+
columns: string[]
|
|
197
|
+
rows: (string | number)[][]
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<div className="overflow-x-auto">
|
|
201
|
+
<table className="w-full text-[11px]">
|
|
202
|
+
<thead>
|
|
203
|
+
<tr className="border-b border-border">
|
|
204
|
+
{columns.map((c, i) => (
|
|
205
|
+
<th key={i} className="text-left py-1 px-2 font-medium text-muted-foreground">
|
|
206
|
+
{c}
|
|
207
|
+
</th>
|
|
208
|
+
))}
|
|
209
|
+
</tr>
|
|
210
|
+
</thead>
|
|
211
|
+
<tbody>
|
|
212
|
+
{rows.map((row, i) => (
|
|
213
|
+
<tr key={i} className="border-b border-border/40 hover:bg-accent/30">
|
|
214
|
+
{row.map((cell, j) => (
|
|
215
|
+
<td key={j} className="py-1 px-2">
|
|
216
|
+
{cell}
|
|
217
|
+
</td>
|
|
218
|
+
))}
|
|
219
|
+
</tr>
|
|
220
|
+
))}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of direct frontend actions that agent-rendered Buttons can trigger.
|
|
3
|
+
* Each action is an async handler that runs in the browser (usually a Supabase
|
|
4
|
+
* write) and returns a short confirmation label for the button's success state.
|
|
5
|
+
*/
|
|
6
|
+
import { supabase } from '../../../lib/supabase'
|
|
7
|
+
|
|
8
|
+
export type ActionHandler = (payload: any) => Promise<string>
|
|
9
|
+
|
|
10
|
+
async function saveAlternative(payload: any): Promise<string> {
|
|
11
|
+
const {
|
|
12
|
+
product_id,
|
|
13
|
+
company_id,
|
|
14
|
+
supplier,
|
|
15
|
+
title,
|
|
16
|
+
url,
|
|
17
|
+
thumbnail,
|
|
18
|
+
price,
|
|
19
|
+
moq,
|
|
20
|
+
review_score,
|
|
21
|
+
review_count,
|
|
22
|
+
gold_supplier_years,
|
|
23
|
+
country,
|
|
24
|
+
} = payload || {}
|
|
25
|
+
|
|
26
|
+
if (!company_id || !supplier) throw new Error('company_id y supplier requeridos')
|
|
27
|
+
|
|
28
|
+
const { error } = await supabase.from('product_alternatives').upsert(
|
|
29
|
+
{
|
|
30
|
+
product_id: product_id || null,
|
|
31
|
+
company_id,
|
|
32
|
+
supplier,
|
|
33
|
+
title: title || null,
|
|
34
|
+
url: url || null,
|
|
35
|
+
thumbnail: thumbnail || null,
|
|
36
|
+
price: price || null,
|
|
37
|
+
moq: moq || null,
|
|
38
|
+
review_score: review_score ?? null,
|
|
39
|
+
review_count: review_count ?? null,
|
|
40
|
+
gold_supplier_years: gold_supplier_years ?? null,
|
|
41
|
+
country: country || null,
|
|
42
|
+
source: 'alibaba',
|
|
43
|
+
},
|
|
44
|
+
{ onConflict: 'product_id,supplier,url' },
|
|
45
|
+
)
|
|
46
|
+
if (error) throw error
|
|
47
|
+
return `✓ ${supplier} guardado como alternativa`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ACTIONS: Record<string, ActionHandler> = {
|
|
51
|
+
save_alternative: saveAlternative,
|
|
52
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { supabase } from '../lib/supabase'
|
|
3
|
+
import type { User } from '@supabase/supabase-js'
|
|
4
|
+
|
|
5
|
+
interface Company {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Profile {
|
|
11
|
+
full_name: string | null
|
|
12
|
+
role_title: string | null
|
|
13
|
+
onboarding_completed: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AuthState {
|
|
17
|
+
user: User | null
|
|
18
|
+
role: 'admin' | 'client' | null
|
|
19
|
+
companyId: string | null
|
|
20
|
+
companies: Company[]
|
|
21
|
+
profile: Profile | null
|
|
22
|
+
loading: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useAuth(): AuthState & { signOut: () => void; setCompanyId: (id: string) => void; reload: () => Promise<void> } {
|
|
26
|
+
const [state, setState] = useState<AuthState>({
|
|
27
|
+
user: null, role: null, companyId: null, companies: [], profile: null, loading: true,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const loadUser = useCallback(async (user: User | null) => {
|
|
31
|
+
if (!user) {
|
|
32
|
+
setState({ user: null, role: null, companyId: null, companies: [], profile: null, loading: false })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [{ data: owned }, { data: profile }] = await Promise.all([
|
|
37
|
+
supabase.from('companies').select('id, name').eq('owner_id', user.id),
|
|
38
|
+
supabase.from('profiles').select('full_name, role_title, onboarding_completed').eq('id', user.id).maybeSingle(),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
if (owned && owned.length > 0) {
|
|
42
|
+
setState({ user, role: 'admin', companyId: owned[0].id, companies: owned, profile: profile || null, loading: false })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data: memberships } = await supabase
|
|
47
|
+
.from('company_users')
|
|
48
|
+
.select('company_id, companies(id, name)')
|
|
49
|
+
.eq('user_id', user.id)
|
|
50
|
+
|
|
51
|
+
const clientCompanies = (memberships || []).map((m: any) => m.companies).filter(Boolean)
|
|
52
|
+
setState({
|
|
53
|
+
user, role: clientCompanies.length > 0 ? 'client' : null,
|
|
54
|
+
companyId: clientCompanies[0]?.id || null, companies: clientCompanies,
|
|
55
|
+
profile: profile || null, loading: false,
|
|
56
|
+
})
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let active = true
|
|
61
|
+
supabase.auth.getUser().then(({ data: { user } }) => {
|
|
62
|
+
if (active) loadUser(user)
|
|
63
|
+
}).catch(() => {})
|
|
64
|
+
|
|
65
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange((_ev, session) => {
|
|
66
|
+
if (active) loadUser(session?.user || null)
|
|
67
|
+
})
|
|
68
|
+
return () => { active = false; subscription.unsubscribe() }
|
|
69
|
+
}, [loadUser])
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...state,
|
|
73
|
+
signOut: () => supabase.auth.signOut(),
|
|
74
|
+
setCompanyId: (id: string) => setState(s => ({ ...s, companyId: id })),
|
|
75
|
+
reload: async () => {
|
|
76
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
77
|
+
await loadUser(user)
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal data-fetching hook. Replaces the `useEffect(() => fetch().then(setState), [deps])` pattern.
|
|
5
|
+
* Handles stale closures via an abort flag. Components should never useEffect for fetching.
|
|
6
|
+
*/
|
|
7
|
+
export function useData<T>(
|
|
8
|
+
fetcher: (signal: AbortSignal) => Promise<T>,
|
|
9
|
+
deps: readonly unknown[],
|
|
10
|
+
initial: T,
|
|
11
|
+
): { data: T; loading: boolean; error: Error | null } {
|
|
12
|
+
const [data, setData] = useState<T>(initial)
|
|
13
|
+
const [loading, setLoading] = useState(true)
|
|
14
|
+
const [error, setError] = useState<Error | null>(null)
|
|
15
|
+
const mountedRef = useRef(true)
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const controller = new AbortController()
|
|
20
|
+
setLoading(true)
|
|
21
|
+
setError(null)
|
|
22
|
+
fetcher(controller.signal)
|
|
23
|
+
.then(result => {
|
|
24
|
+
if (!controller.signal.aborted) {
|
|
25
|
+
setData(result)
|
|
26
|
+
setLoading(false)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.catch(err => {
|
|
30
|
+
if (!controller.signal.aborted) {
|
|
31
|
+
setError(err)
|
|
32
|
+
setLoading(false)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
return () => controller.abort()
|
|
36
|
+
}, deps)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
mountedRef.current = true
|
|
40
|
+
return () => { mountedRef.current = false }
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
return { data, loading, error }
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run an effect exactly once on mount. The only sanctioned direct useEffect wrapper.
|
|
5
|
+
* Components must never import useEffect directly — use this or another custom hook.
|
|
6
|
+
*/
|
|
7
|
+
export function useMountEffect(effect: () => void | (() => void)) {
|
|
8
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9
|
+
useEffect(effect, [])
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export type Theme = 'light' | 'dark' | 'system'
|
|
4
|
+
|
|
5
|
+
function getStoredTheme(): Theme {
|
|
6
|
+
return (localStorage.getItem('proto-theme') as Theme) || 'dark'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function applyTheme(theme: Theme) {
|
|
10
|
+
const root = document.documentElement
|
|
11
|
+
if (theme === 'system') {
|
|
12
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
13
|
+
root.classList.toggle('dark', isDark)
|
|
14
|
+
} else {
|
|
15
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Apply on load (before React renders)
|
|
20
|
+
applyTheme(getStoredTheme())
|
|
21
|
+
|
|
22
|
+
// Listen for system theme changes
|
|
23
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
24
|
+
if (getStoredTheme() === 'system') applyTheme('system')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function useTheme() {
|
|
28
|
+
const [theme, setThemeState] = useState<Theme>(getStoredTheme)
|
|
29
|
+
|
|
30
|
+
const setTheme = useCallback((t: Theme) => {
|
|
31
|
+
localStorage.setItem('proto-theme', t)
|
|
32
|
+
setThemeState(t)
|
|
33
|
+
applyTheme(t)
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
return { theme, setTheme }
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @proto/core-web — framework library for proto SPAs.
|
|
3
|
+
*
|
|
4
|
+
* Apps import Shell, defineWidget, hooks, lib helpers, and shadcn primitives
|
|
5
|
+
* from this barrel. Nothing in here is Hermes-specific.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Framework components
|
|
9
|
+
export { default as Shell, type CockpitDefinition } from './components/Shell'
|
|
10
|
+
export { ProtoApp, type ProtoAppProps } from './ProtoApp'
|
|
11
|
+
|
|
12
|
+
// Extension API
|
|
13
|
+
export {
|
|
14
|
+
defineWidget,
|
|
15
|
+
buildWidgetRegistry,
|
|
16
|
+
type WidgetDefinition,
|
|
17
|
+
type WidgetRegistry,
|
|
18
|
+
type WidgetCategory,
|
|
19
|
+
type WidgetSize,
|
|
20
|
+
type ShellContext,
|
|
21
|
+
} from './lib/define-widget'
|
|
22
|
+
export type { ActiveEntity, WidgetInstance, WidgetType } from './components/shell/types'
|
|
23
|
+
|
|
24
|
+
// Hooks
|
|
25
|
+
export { useAuth } from './hooks/useAuth'
|
|
26
|
+
export { useData } from './hooks/useData'
|
|
27
|
+
export { useMountEffect } from './hooks/useMountEffect'
|
|
28
|
+
export { useTheme, type Theme } from './hooks/useTheme'
|
|
29
|
+
|
|
30
|
+
// Lib
|
|
31
|
+
export * from './lib/api'
|
|
32
|
+
export * from './lib/config'
|
|
33
|
+
export { supabase } from './lib/supabase'
|
|
34
|
+
export { cn } from './lib/utils'
|
|
35
|
+
export * from './lib/drag'
|
|
36
|
+
export * from './lib/widgetCache'
|
|
37
|
+
|
|
38
|
+
// Agent runtime (render_ui)
|
|
39
|
+
export { Generative } from './components/widgets/agent/Generative'
|
|
40
|
+
|
|
41
|
+
// UI primitives (shadcn)
|
|
42
|
+
export { Avatar, AvatarFallback } from './components/ui/avatar'
|
|
43
|
+
export { Badge } from './components/ui/badge'
|
|
44
|
+
export { Button } from './components/ui/button'
|
|
45
|
+
export { Card, CardContent, CardFooter, CardHeader } from './components/ui/card'
|
|
46
|
+
export { InlineEdit } from './components/ui/inline-edit'
|
|
47
|
+
export { Input } from './components/ui/input'
|
|
48
|
+
export { ScrollArea } from './components/ui/scroll-area'
|
|
49
|
+
export { Separator } from './components/ui/separator'
|
|
50
|
+
export { ShellDialog } from './components/ui/shell-dialog'
|
|
51
|
+
export { Skeleton } from './components/ui/skeleton'
|
|
52
|
+
export { Textarea } from './components/ui/textarea'
|