Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
860 changes: 827 additions & 33 deletions ui/consumer/bun.lock

Large diffs are not rendered by default.

44 changes: 43 additions & 1 deletion ui/consumer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,52 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@conform-to/react": ">=1.19.4 <2",
"@conform-to/zod": ">=1.19.4 <2",
"@datum-cloud/assistant-chat-kit": "^0.1.0",
"@datum-cloud/datum-ui": "^2.5.0",
"@datum-cloud/portal-plugin-sdk": "^1.0.0",
"@dnd-kit/core": ">=6.3.1 <7",
"@dnd-kit/sortable": ">=10 <11",
"@hookform/resolvers": ">=5.4.0 <6",
"@monaco-editor/react": ">=4.7.0 <5",
"@stepperize/react": ">=7 <8",
"@streamdown/code": "^1.1.1",
"@tanstack/react-query": "5.101.4",
"@tanstack/react-table": ">=9 <10",
"@tanstack/react-virtual": ">=3.14.3 <4",
"@tiptap/extension-character-count": ">=3.27.1 <4",
"@tiptap/extension-link": ">=3.27.1 <4",
"@tiptap/extension-placeholder": ">=3.27.1 <4",
"@tiptap/extension-underline": ">=3.27.1 <4",
"@tiptap/react": ">=3.27.1 <4",
"@tiptap/starter-kit": ">=3.27.1 <4",
"ai": "^7.0.97",
"date-fns": ">=4.1.0 <5",
"date-fns-tz": ">=3 <4",
"js-yaml": ">=5.2.2 <6",
"leaflet": ">=1.9 <2",
"leaflet-draw": ">=1 <2",
"leaflet.fullscreen": ">=5 <6",
"leaflet.markercluster": ">=1.5.3 <2",
"lucide-react": "^1.21.0",
"monaco-editor": ">=0.44.0 <1",
"motion": "^13.2.0",
"nprogress": ">=0.2 <1",
"nuqs": ">=2 <3",
"react": "19.2.8",
"react-day-picker": ">=10 <11",
"react-dom": "19.2.8",
"react-dropzone": ">=15 <21",
"react-hook-form": ">=7.80.0 <8",
"react-leaflet": ">=5 <6",
"react-leaflet-markercluster": ">=5.0.0-rc.0 <6",
"react-number-format": ">=5.4.5 <6",
"react-router": "7.18.0",
"@tanstack/react-query": "5.101.4"
"recharts": ">=3 <4",
"sonner": ">=2.0.7 <3",
"streamdown": "^2.6.0",
"zod": ">=4 <5"
},
"devDependencies": {
"@module-federation/vite": "^1.16.16",
Expand Down
11 changes: 10 additions & 1 deletion ui/consumer/public/plugin-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"range": "^1.0.0"
},
"remoteEntry": "remoteEntry.js",
"exposedModules": {},
"exposedModules": {
"Assistant": "./src/pages/assistant.tsx"
},
"extensions": [
{
"type": "portal.nav/project",
Expand All @@ -21,6 +23,13 @@
"comingSoonMode": "plugin",
"serviceRef": "interconnect.datumapis.com"
}
},
{
"type": "portal.page/project",
"properties": {
"path": "assistant",
"component": { "$codeRef": "Assistant" }
}
}
]
}
Empty file removed ui/consumer/src/lib/.gitkeep
Empty file.
36 changes: 36 additions & 0 deletions ui/consumer/src/lib/assistant-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defaultRenderLink } from '@datum-cloud/datum-ui/assistant';
import type { AssistantConfig } from '@datum-cloud/datum-ui/assistant';

/**
* This plugin's concrete `AssistantConfig` ("Patch"), mirroring the assistant
* plugin's own `assistant-config.ts` but with copy specific to interconnect
* rather than the generic project-scoped defaults — the backend it talks to
* is the same Patch service, scoped to interconnect tools/skills via that
* service's own `AgentBinding` capability document.
*/

/** Starter prompts on the empty/new-chat state. */
export const SUGGESTIONS = [
'What interconnects do I have in this project?',
'Help me diagnose a degraded interconnect.',
'What can Patch help me with here?',
] as const;

/**
* Tool name → progress label shown while a tool call is running. Starts
* empty, same reasoning as the assistant plugin's own config — this
* plugin's backend tool set isn't fixed/known ahead of time.
*/
export const TOOL_LABELS: Record<string, string> = {};

export const ASSISTANT_CONFIG: AssistantConfig = {
greeting: (name) => `Hey there${name ? `, ${name}` : ''}`,
suggestions: [...SUGGESTIONS],
// No reasoning-part source on this transport (the apiserver's SSE envelope
// has no `reasoning`/`thinking` event) — hide the toggle rather than fake it.
showReasoning: false,
// No model/effort picker — the server side (a2a runner) chooses the model.
modelSelector: false,
toolLabels: TOOL_LABELS,
renderLink: defaultRenderLink,
};
Empty file removed ui/consumer/src/pages/.gitkeep
Empty file.
97 changes: 97 additions & 0 deletions ui/consumer/src/pages/assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { AssistantWorkspace } from '@datum-cloud/datum-ui/assistant';
import { usePluginFetch, useProjectContext } from '@datum-cloud/portal-plugin-sdk';
import { useAssistantWorkspace } from '@datum-cloud/assistant-chat-kit';

import { ASSISTANT_CONFIG } from '../lib/assistant-config';

/**
* `Assistant` — the module exposed as `interconnect.datumapis.com/Assistant`,
* `$codeRef`'d by the `portal.page/project` extension in
* `public/plugin-manifest.json` (routed at `.../services/interconnect/assistant`).
*
* Full-page counterpart to the assistant plugin's docked `ChatDock`: same
* conversation logic (`@datum-cloud/assistant-chat-kit`'s
* `useAssistantWorkspace`, talking to the same per-project Patch backend —
* interconnect-specific answers come from that backend's own `AgentBinding`
* capability document, not from anything here), same presentational shell
* (`@datum-cloud/datum-ui/assistant`'s `AssistantWorkspace`), just laid out to
* fill a routed page instead of a docked panel.
*
* Reads its host wiring from `@datum-cloud/portal-plugin-sdk`'s
* `usePluginFetch()`/`useProjectContext()` rather than props, same reason as
* `ChatDock`: a Module Federation `$codeRef` is mounted generically by the
* host with no way to pass props in.
*/
export default function Assistant() {
const { project } = useProjectContext();
const pluginFetch = usePluginFetch();
const projectName = project?.name ?? '';
const workspace = useAssistantWorkspace({ pluginFetch, projectName });

return (
<div
data-testid="interconnect-assistant-page"
aria-label="Patch AI"
style={{ position: 'relative', height: '100%', width: '100%' }}
>
<AssistantWorkspace
config={ASSISTANT_CONFIG}
title={workspace.title}
messages={workspace.messages}
status={workspace.status}
error={workspace.error}
isReady={workspace.isReady}
chatList={workspace.chatList}
currentChatId={workspace.currentChatId}
sidebarHeader={
project ? (
<div style={{ minWidth: 0 }}>
<p style={{ opacity: 0.5, fontSize: '0.7rem' }}>Project</p>
<p style={{ fontSize: '0.75rem', fontWeight: 500 }}>
{project.displayName ?? project.name}
</p>
</div>
) : undefined
}
editor={workspace.editor}
htmlByUserMsgIndex={workspace.htmlByUserMsgIndex}
bottomRef={workspace.bottomRef}
containerRef={workspace.containerRef}
userScrolledUpRef={workspace.userScrolledUpRef}
onSend={workspace.onSend}
onStop={workspace.onStop}
onRetry={workspace.onRetry}
onNewChat={workspace.onNewChat}
onLoadChat={workspace.onLoadChat}
onArchiveChat={workspace.onArchiveChat}
onUnarchiveChat={workspace.onUnarchiveChat}
onDeleteChat={workspace.onDeleteChat}
confirmDelete
onSuggestion={workspace.onSuggestion}
modelId={workspace.modelId}
effortId={workspace.effortId}
onModelChange={workspace.onModelChange}
onEffortChange={workspace.onEffortChange}
micSupported={workspace.micSupported}
micListening={workspace.micListening}
micFrequencyData={workspace.micFrequencyData}
onMicToggle={workspace.onMicToggle}
historyOpen={workspace.historyOpen}
onToggleHistory={workspace.onToggleHistory}
/>
{workspace.error && workspace.messages.length === 0 && (
<p
role="alert"
style={{ position: 'absolute', top: 0, left: 0, right: 0, padding: '0.5rem', textAlign: 'center', fontSize: '0.75rem', color: 'var(--destructive, #dc2626)' }}
>
{workspace.error.message}
</p>
)}
{!project && (
<p role="status" style={{ position: 'absolute', bottom: 0, left: 0, right: 0, opacity: 0.7 }}>
No project context supplied — running with an empty project scope.
</p>
)}
</div>
);
}
19 changes: 18 additions & 1 deletion ui/consumer/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,31 @@ export default defineConfig({
// The manifest's `remoteEntry` field points the host at this filename.
filename: 'remoteEntry.js',
manifest: true,
exposes: {},
exposes: {
'./Assistant': './src/pages/assistant.tsx',
},
// Host-pinned singletons — the host provides all of these, so plugin
// queries share the host's QueryClient cache.
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
// React 19's createRoot/hydrateRoot live in this subpath, which MF
// treats as its own distinct shared module from 'react-dom'. Left
// undeclared, MF crashes with React's "incompatible react/react-dom
// versions" invariant instead of just using a local fallback — see
// assistant/ui/consumer/vite.config.ts's comment on this, where it
// bit that plugin first. Needs a matching host-side entry in
// cloud-portal's federation-host.ts hostShared().
'react-dom/client': { singleton: true, requiredVersion: '^19.0.0' },
'react-router': { singleton: true, requiredVersion: '^7.0.0' },
'@tanstack/react-query': { singleton: true, requiredVersion: '^5.0.0' },
// Must be shared, not bundled: useProjectContext()/usePluginFetch()
// read a React Context defined inside this module, which is only the
// host's PortalPluginHostProvider's Context if every consumer
// resolves the host's exact module instance instead of this plugin's
// own copy (see assistant/ui/consumer/vite.config.ts, same
// requirement there).
'@datum-cloud/portal-plugin-sdk': { singleton: true, requiredVersion: '^1.0.0' },
},
}),
],
Expand Down
Loading