From 12b9d1e6d9e4e48efb71449a5deda9047439b4bb Mon Sep 17 00:00:00 2001 From: AppHub Developer Date: Mon, 22 Jun 2026 20:22:38 +0300 Subject: [PATCH 1/2] feat(chat-ui): @fuzefront/chat-ui design-system chat frontend + shell integration Squashed onto current master to clear a stale GitHub CONFLICTING flag (compare showed behind_by=0; merge-commit history confused mergeability). Net content unchanged: chat-ui package consumed via file: deps + tsconfig dist paths, CI builds chat-client+chat-ui before frontend type-check/bundle, ignoreDeprecations for tsup DTS (TS 6.0.3 baseUrl), linux-regenerated lockfiles. --- .github/workflows/ci.yml | 16 + .gitignore | 8 +- deploy/helm/fuzefront/values.yaml | 37 + frontend/nginx.conf | 18 + frontend/package-lock.json | 50 ++ frontend/package.json | 2 + frontend/src/App.tsx | 19 +- frontend/src/components/ChatPanel.tsx | 162 ---- frontend/src/components/FuzeChatWidget.tsx | 47 ++ frontend/src/components/Layout.tsx | 19 +- frontend/src/components/TopBar.tsx | 18 +- frontend/src/contexts/ChatContext.tsx | 222 ------ frontend/src/types/chat.ts | 29 - frontend/tsconfig.json | 58 +- frontend/vite.config.ts | 8 +- lerna.json | 2 +- package-lock.json | 722 ++++++++++++++++++ package.json | 1 + packages/chat-ui/.gitignore | 3 + packages/chat-ui/README.md | 65 ++ packages/chat-ui/package.json | 60 ++ packages/chat-ui/scripts/copy-css.mjs | 14 + packages/chat-ui/src/components/ChatPanel.tsx | 65 ++ .../chat-ui/src/components/ChatWidget.tsx | 83 ++ packages/chat-ui/src/components/Citations.tsx | 34 + packages/chat-ui/src/components/Composer.tsx | 59 ++ .../src/components/ConfirmationCard.tsx | 60 ++ .../src/components/FeedbackButtons.tsx | 34 + .../chat-ui/src/components/MessageItem.tsx | 63 ++ .../chat-ui/src/components/MessageList.tsx | 42 + packages/chat-ui/src/hooks/chatReducer.ts | 203 +++++ packages/chat-ui/src/hooks/types.ts | 37 + packages/chat-ui/src/hooks/useChat.ts | 112 +++ packages/chat-ui/src/i18n/index.ts | 93 +++ packages/chat-ui/src/index.ts | 40 + packages/chat-ui/src/styles/chat-ui.css | 368 +++++++++ packages/chat-ui/tests/chatReducer.test.ts | 109 +++ packages/chat-ui/tests/components.test.tsx | 181 +++++ packages/chat-ui/tests/fakeClient.ts | 50 ++ packages/chat-ui/tests/setup.ts | 6 + packages/chat-ui/tests/useChat.test.tsx | 103 +++ packages/chat-ui/tsconfig.json | 38 + packages/chat-ui/tsup.config.ts | 22 + packages/chat-ui/vitest.config.ts | 13 + services/chat-service/src/agent/executor.ts | 128 ++++ .../chat-service/src/agent/tools/mutating.ts | 80 ++ .../src/agent/tools/platform-client.ts | 72 ++ .../chat-service/src/db/repositories/audit.ts | 42 + .../chat-service/tests/agent/executor.test.ts | 117 +++ .../chat-service/tests/agent/mutating.test.ts | 72 ++ .../tests/db/repositories/audit.test.ts | 65 ++ 51 files changed, 3481 insertions(+), 490 deletions(-) delete mode 100644 frontend/src/components/ChatPanel.tsx create mode 100644 frontend/src/components/FuzeChatWidget.tsx delete mode 100644 frontend/src/contexts/ChatContext.tsx delete mode 100644 frontend/src/types/chat.ts create mode 100644 packages/chat-ui/.gitignore create mode 100644 packages/chat-ui/README.md create mode 100644 packages/chat-ui/package.json create mode 100644 packages/chat-ui/scripts/copy-css.mjs create mode 100644 packages/chat-ui/src/components/ChatPanel.tsx create mode 100644 packages/chat-ui/src/components/ChatWidget.tsx create mode 100644 packages/chat-ui/src/components/Citations.tsx create mode 100644 packages/chat-ui/src/components/Composer.tsx create mode 100644 packages/chat-ui/src/components/ConfirmationCard.tsx create mode 100644 packages/chat-ui/src/components/FeedbackButtons.tsx create mode 100644 packages/chat-ui/src/components/MessageItem.tsx create mode 100644 packages/chat-ui/src/components/MessageList.tsx create mode 100644 packages/chat-ui/src/hooks/chatReducer.ts create mode 100644 packages/chat-ui/src/hooks/types.ts create mode 100644 packages/chat-ui/src/hooks/useChat.ts create mode 100644 packages/chat-ui/src/i18n/index.ts create mode 100644 packages/chat-ui/src/index.ts create mode 100644 packages/chat-ui/src/styles/chat-ui.css create mode 100644 packages/chat-ui/tests/chatReducer.test.ts create mode 100644 packages/chat-ui/tests/components.test.tsx create mode 100644 packages/chat-ui/tests/fakeClient.ts create mode 100644 packages/chat-ui/tests/setup.ts create mode 100644 packages/chat-ui/tests/useChat.test.tsx create mode 100644 packages/chat-ui/tsconfig.json create mode 100644 packages/chat-ui/tsup.config.ts create mode 100644 packages/chat-ui/vitest.config.ts create mode 100644 services/chat-service/src/agent/executor.ts create mode 100644 services/chat-service/src/agent/tools/mutating.ts create mode 100644 services/chat-service/src/agent/tools/platform-client.ts create mode 100644 services/chat-service/src/db/repositories/audit.ts create mode 100644 services/chat-service/tests/agent/executor.test.ts create mode 100644 services/chat-service/tests/agent/mutating.test.ts create mode 100644 services/chat-service/tests/db/repositories/audit.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0aa09ec51..aa1a7c8ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,15 @@ jobs: - name: Build @fuzefront/identity-ui (d.ts for frontend type-check) run: npm run -w @fuzefront/identity-ui build + # @fuzefront/chat-ui is consumed by the host frontend (FuzeChatWidget) and + # has NO vite source alias, so both the frontend type-check (tsconfig → + # chat-ui/dist/index.d.ts) and `vite build` resolve it from dist. Build its + # dependency @fuzefront/chat-client first, then chat-ui. + - name: Build @fuzefront/chat-client + chat-ui (for frontend type-check) + run: | + npm run -w @fuzefront/chat-client build + npm run -w @fuzefront/chat-ui build + # @fuzefront/i18n is consumed by the host (main.tsx, contexts, components). # frontend/tsconfig.json resolves it to dist/index.d.ts (like identity-ui), # so build it before the frontend type-check. vite/vitest still alias it to @@ -151,6 +160,13 @@ jobs: - name: Build backend run: cd backend && npm run build + # chat-ui is bundled into the host frontend from its dist (no vite source + # alias), and depends on chat-client — build both before `vite build`. + - name: Build @fuzefront/chat-client + chat-ui (for frontend bundle) + run: | + npm run -w @fuzefront/chat-client build + npm run -w @fuzefront/chat-ui build + - name: Build frontend run: cd frontend && npm run build diff --git a/.gitignore b/.gitignore index d92316779..d07c3de1b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ frontend/tests/_screens.spec.ts # Local dev CA public cert (per-machine; regenerate via deploy/local-tls/) deploy/local-tls/*.crt +# Isolated per-worktree npm caches (parallel-agent gotcha) +.npm-cache-*/ + # Build output for the @fuzefront/* npm packages (regenerated by `npm run build`) packages/*/dist/ @@ -39,6 +42,5 @@ packages/*/.npmrc # Stray npm pack tarballs *.tgz -# Per-package lockfiles (generated locally; CI installs fresh). Avoids leaking -# the Windows native-binary pins from a local install into Linux CI. -packages/*/package-lock.json \ No newline at end of file +# Per-package lockfiles (generated locally; CI installs fresh). +packages/*/package-lock.json diff --git a/deploy/helm/fuzefront/values.yaml b/deploy/helm/fuzefront/values.yaml index e3382a859..86666eed7 100644 --- a/deploy/helm/fuzefront/values.yaml +++ b/deploy/helm/fuzefront/values.yaml @@ -286,6 +286,43 @@ chatService: # Directory inside the chat-service image that holds the docs to index. docsDir: "/app/services/chat-service/docs" +chatService: + enabled: false + replicas: 1 + # Port 3006: 3001=backend, 3002=security, 3003=apps/email, 3004=sms, 3005=provisioning + port: 3006 + image: + repository: fuzefront/chat-service + tag: local + # LiteLLM gateway in fuzeinfra namespace (its own Argo App). + litellmUrl: "http://litellm.fuzeinfra.svc.cluster.local:4000" + # ChromaDB vector store in fuzeinfra namespace (enabled via fuzeinfra Argo values). + chromaUrl: "http://chromadb.fuzeinfra.svc.cluster.local:8000" + # FuzeFront backend for user/org lookups. + backendUrl: "http://fuzefront-backend:3001" + # Permit PDP for authorization checks. When permit.enabled=true the in-cluster PDP runs + # at fuzefront-permit-pdp:7000; otherwise falls back to the offline stub port. + permitPdpUrl: "http://fuzefront-permit-pdp:7000" + kafka: + brokers: "kafka.fuzeinfra.svc.cluster.local:9092" + # Embedding model resolved at the LiteLLM gateway (used by the doc indexer). + embeddingModel: "text-embedding-3-small" + # Resource requests/limits. Local/dev single replica; prod overrides upward. + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + # chat-doc-indexer Job: indexes docs/ into ChromaDB as a post-install/upgrade + # hook. Idempotent (content-hash keyed). Disabled by default — flip on once + # LiteLLM + ChromaDB are reachable in fuzeinfra. + docIndexer: + enabled: false + # Directory inside the chat-service image that holds the docs to index. + docsDir: "/app/services/chat-service/docs" + # Plan D — thin Kafka→HTTP bridge: consumes identity.user.created, calls # security-service POST /internal/provision. No DB, no domain logic. # SECURITY_SERVICE_URL is derived in the Helm template from securityService.port diff --git a/frontend/nginx.conf b/frontend/nginx.conf index af5763881..c6dbb295b 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -114,6 +114,24 @@ http { proxy_read_timeout 86400; } + # Chat-service: AI chat + RAG. SSE streaming, so buffering MUST be off and + # the read timeout long. baseUrl on the client is `${origin}/chat-api`, so + # /chat-api/chat/stream maps to the service's /chat/stream. + location /chat-api/ { + proxy_pass http://fuzefront-chat-service:3006/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # SSE: disable proxy + nginx buffering so text_delta events flush live. + proxy_buffering off; + proxy_cache off; + proxy_set_header Connection ''; + chunked_transfer_encoding off; + proxy_read_timeout 86400; + } + # WebSocket proxy -> applications-service (owns Socket.IO). location /socket.io/ { proxy_pass http://fuzefront-applications:3003; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 097ba1f4f..eb9215b49 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "@fuzefront/frontend", "version": "1.0.0", "dependencies": { + "@fuzefront/chat-client": "file:../packages/chat-client", + "@fuzefront/chat-ui": "file:../packages/chat-ui", "@originjs/vite-plugin-federation": "^1.4.1", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", @@ -51,6 +53,46 @@ "@rollup/rollup-win32-x64-msvc": "^4.62.0" } }, + "../packages/chat-client": { + "name": "@fuzefront/chat-client", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^1.1.2" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/node": "18.19.0", + "jest": "29.7.0", + "ts-jest": "29.1.1", + "typescript": "5.1.6" + } + }, + "../packages/chat-ui": { + "name": "@fuzefront/chat-ui", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@fuzefront/chat-client": "file:../chat-client", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.1", + "jsdom": "^24.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tsup": "^8.0.2", + "typescript": "5.1.6", + "vitest": "^1.6.0" + }, + "peerDependencies": { + "@fuzefront/chat-client": "^1.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", @@ -911,6 +953,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fuzefront/chat-client": { + "resolved": "../packages/chat-client", + "link": true + }, + "node_modules/@fuzefront/chat-ui": { + "resolved": "../packages/chat-ui", + "link": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1e3375197..c2bbc7d76 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,8 @@ "test:e2e:report": "playwright show-report" }, "dependencies": { + "@fuzefront/chat-client": "file:../packages/chat-client", + "@fuzefront/chat-ui": "file:../packages/chat-ui", "@originjs/vite-plugin-federation": "^1.4.1", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a5f44780d..cb8785f81 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import { useCurrentUser, useAppContext, MenuItem } from './lib/shared' import { installBridge, bridge } from './platform/bridge' -import { ChatProvider } from './contexts/ChatContext' import Layout from './components/Layout' import LoginPage from './pages/LoginPage' import DashboardPage from './pages/DashboardPage' @@ -16,7 +15,7 @@ import { getCurrentUser } from './services/api' import websocketService from './services/websocket' import { UserProfileManagement } from './components/UserProfileManagement' import { WorkspaceProvisioningGate } from './components/WorkspaceProvisioningGate' -import CreateOrganizationPage from './pages/CreateOrganizationPage' +import CreateOrganizationPage from './pages/CreateOrganizationPage' import AcceptInvitePage from './pages/AcceptInvitePage' // Authentication wrapper component @@ -174,13 +173,13 @@ function App() { ) } -function AppContent() { - const { isAuthenticated, user } = useCurrentUser() - const currentPath = typeof window !== 'undefined' ? window.location.pathname : '' - - // Public route: invitation accept page — handle before auth check - if (currentPath.startsWith('/invitations/')) { - return +function AppContent() { + const { isAuthenticated, user } = useCurrentUser() + const currentPath = typeof window !== 'undefined' ? window.location.pathname : '' + + // Public route: invitation accept page — handle before auth check + if (currentPath.startsWith('/invitations/')) { + return } console.log('AppContent - Authentication state:', { @@ -196,7 +195,6 @@ function AppContent() { console.log('User authenticated, showing main app') return ( - } /> @@ -212,7 +210,6 @@ function AppContent() { } /> - ) } diff --git a/frontend/src/components/ChatPanel.tsx b/frontend/src/components/ChatPanel.tsx deleted file mode 100644 index 3cc950e68..000000000 --- a/frontend/src/components/ChatPanel.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react' -import { ChatPanelProps } from '../types/chat' - -const ChatPanel: React.FC = ({ - isOpen, - onToggle, - messages = [], - onSendMessage, - onFeedback, - isLoading = false, -}) => { - const [inputValue, setInputValue] = useState('') - const messagesEndRef = useRef(null) - - // Auto-scroll to bottom when new messages arrive - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) - }, [messages]) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (inputValue.trim() && !isLoading) { - onSendMessage(inputValue.trim()) - setInputValue('') - } - } - - const formatTime = (timestamp: Date) => { - return timestamp.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }) - } - - const handleFeedback = (messageId: string, type: 'positive' | 'negative') => { - onFeedback?.(messageId, type) - } - - return ( -
- {/* Chat Panel Header */} -
-

🤖 AI Assistant

- -
- - {/* Chat Messages Area */} - {isOpen && ( - <> -
- {messages.length === 0 ? ( -
-
- 👋 Hello! I'm your AI assistant. How can I help you with - FrontFuse today? -
-
- ) : ( - messages.map(message => ( -
-
- - {message.sender === 'user' ? '👤 You' : '🤖 AI'} - - - {formatTime(message.timestamp)} - -
- -
{message.content}
- - {/* Feedback for AI messages */} - {message.sender === 'assistant' && ( -
- - -
- )} -
- )) - )} - - {/* Loading indicator */} - {isLoading && ( -
-
- 🤖 AI - typing... -
-
- - - - - -
-
- )} - -
-
- - {/* Chat Input Area */} -
-
-
-