From 977cc7b839bc293310e8eac26a03b08d5324b6a6 Mon Sep 17 00:00:00 2001 From: ashbuilds Date: Wed, 22 Oct 2025 23:45:56 +0200 Subject: [PATCH 1/7] [feat] compose agent chat ui - wip --- dev/app/(payload)/admin/importMap.js | 4 +- dev/dev.db | Bin 421888 -> 421888 bytes src/exports/client.ts | 1 + src/plugin.ts | 3 + src/providers/AgentProvider/AgentProvider.tsx | 31 ++++ src/ui/AgentSidebar/AgentSidebar.tsx | 59 +++++++ src/ui/AgentSidebar/agent-sidebar.module.css | 149 ++++++++++++++++++ 7 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/providers/AgentProvider/AgentProvider.tsx create mode 100644 src/ui/AgentSidebar/AgentSidebar.tsx create mode 100644 src/ui/AgentSidebar/agent-sidebar.module.css diff --git a/dev/app/(payload)/admin/importMap.js b/dev/app/(payload)/admin/importMap.js index ce09f61..f9f0113 100644 --- a/dev/app/(payload)/admin/importMap.js +++ b/dev/app/(payload)/admin/importMap.js @@ -28,6 +28,7 @@ import { TableFeatureClient as TableFeatureClient_e70f5e05f09f93e00b997edb1ef0c8 import { SelectField as SelectField_c25bd927cd468b8e16d7bdb2cc282659 } from '@ai-stack/payloadcms/fields' import { PromptEditorField as PromptEditorField_c25bd927cd468b8e16d7bdb2cc282659 } from '@ai-stack/payloadcms/fields' import { InstructionsProvider as InstructionsProvider_4490b89d4413c1ffaecdacfe72efaf73 } from '@ai-stack/payloadcms/client' +import { AgentProvider as AgentProvider_4490b89d4413c1ffaecdacfe72efaf73 } from '@ai-stack/payloadcms/client' export const importMap = { "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, @@ -59,5 +60,6 @@ export const importMap = { "@payloadcms/richtext-lexical/client#TableFeatureClient": TableFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@ai-stack/payloadcms/fields#SelectField": SelectField_c25bd927cd468b8e16d7bdb2cc282659, "@ai-stack/payloadcms/fields#PromptEditorField": PromptEditorField_c25bd927cd468b8e16d7bdb2cc282659, - "@ai-stack/payloadcms/client#InstructionsProvider": InstructionsProvider_4490b89d4413c1ffaecdacfe72efaf73 + "@ai-stack/payloadcms/client#InstructionsProvider": InstructionsProvider_4490b89d4413c1ffaecdacfe72efaf73, + "@ai-stack/payloadcms/client#AgentProvider": AgentProvider_4490b89d4413c1ffaecdacfe72efaf73 } diff --git a/dev/dev.db b/dev/dev.db index dafe376c9ae1257a6d4a5d47a00bc9fa835a6e12..e5561d6336b8188b26a2ed7aa029a758682a0517 100644 GIT binary patch delta 1783 zcmd5-U1$_n6ux)v%sn1mQX z(2xYdKE+sQowufx5}^ob33VR&V46ZBi1-hZkQh*F{0Wu#69a|x++Cxa&2ETq?!%p( zbI$$lckcPlnV#wLp6T*4AJZG(R_vfRe%t@^#UkckAn*e?{)ORnfhYc*Uowp>{T!#v zG|adjGj!AZ%8b?6u^PjU7*?XbkUiPa(A+ufTpbI0;s_OLf0u<{D?_=vSp{>bTiZ`V z-gKpK6E@^IAv-22gac=Ls8}Lu=Lj71U-N&WokPB^)_qJ25}HNWaa4sBVpqtXW}Vk* zTlrH(d+%o~NW4v_=@cyy;W2^#;1xWB$H;3O+-Cz+6qv}LrRbjJ?s=GlS#(c=Br-xp zZ*GVN11r6sroDV&d=VHUY7yqR0_KAZ>?3d=#^4v|!&JM#Z6AnSUlNHru9$>wA{Te`-ZF0HCsE?x7T-CK;ZN!Yj+MX!W^9w^T+gcUQL*>3%yFVu?tV z9naHg)se07>iS)>O!Re*)6~||l4|Na)YhsWP9+aD9Pi3bJXT{>*H~sGX5}I++3orh zOeVYQ-XM9ex+K34@If7cRv3b(a1w4qofqhlc7S`bUka63zEY;s+;e5_><#)L!KL1Z zQXhkM*aHd7GMo{7BsVu77knYQGH;UamH^YeQy9?5T*@kdc0B?Pr2z*Cynrh(4i8}j zhG7sHo`H{wRhbYPQhi5{q&kX~%!|xl+>wjYP9!qL=N0M`1w8}zU<&tLf+9YRRK6(g z%vGrZFBg!(kAy_rf`3uIdc4SDQ6o21=rJ{yQs^lm^!P_QPgAE02oD;5gsrHvkR z#DfJ95fNKk+CB8pqF_$aB2`f+22C{a14FFFLm|Gj)>cvQ=xyfzd++_lX zP_7T*E6Sw4f-Dd7&Esl$36rB-_0UO4*p37rg}gOH&JVrHR$EQ*5v+9bpi|jqt7*Q3 z%iW4vCu6ITJ69st-^s1QP$j<#x#eM~Lgzvi&idqaeXP8QhBd*dU-u*?=af4=yR1lW zuqzS@`1*R2VPDXS9`>c8sYAYGAd*Uk`ohU%s>?F1kk4=W%RtyaT3?uk}0X)G~A}1mDX0%3mC8HfDny&AB5Vw&=8Ops2IWw*8ptNFr%O6pp=@$t+g)Gi$Ed=sE){l3{6L<_e z|59u95SeHAh6TLAC(IMM4>@1c%s = ({ children }) => { + const [open, setOpen] = React.useState(true) + + return ( + <> + {children} + + {!open && ( + + )} + + setOpen(false)} open={open} /> + + ) +} + +export default AgentProvider diff --git a/src/ui/AgentSidebar/AgentSidebar.tsx b/src/ui/AgentSidebar/AgentSidebar.tsx new file mode 100644 index 0000000..e340acb --- /dev/null +++ b/src/ui/AgentSidebar/AgentSidebar.tsx @@ -0,0 +1,59 @@ +'use client' + +import React from 'react' + +import styles from './agent-sidebar.module.css' + +type AgentSidebarProps = { + onCloseAction?: () => void + open?: boolean +} + +export const AgentSidebar: React.FC = ({ onCloseAction, open = false }) => { + return ( +
+
+
+ AI + Agent +
+ +
+ +
+
+

Agent chat

+

+ UI placeholder. Streaming and persistence will be added later. +

+
+
+ +
+ + +
+
+ ) +} + +export default AgentSidebar diff --git a/src/ui/AgentSidebar/agent-sidebar.module.css b/src/ui/AgentSidebar/agent-sidebar.module.css new file mode 100644 index 0000000..1cedb56 --- /dev/null +++ b/src/ui/AgentSidebar/agent-sidebar.module.css @@ -0,0 +1,149 @@ +:root { + --agent-nav-width: 96px; + --agent-panel-width: 340px; +} + +.panel { + position: fixed; + top: 0; + right: 0; + height: 100vh; + width: var(--agent-panel-width); + background: var(--theme-elevation-0); + border-right: 1px solid var(--theme-elevation-150); + box-shadow: 0 0 0 1px var(--theme-elevation-150), 0 8px 24px rgba(0, 0, 0, 0.08); + transform: translateX(100%); + transition: transform 200ms ease; + z-index: 30; + display: flex; + flex-direction: column; +} + +.open { + transform: translateX(0); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 12px 12px; + border-bottom: 1px solid var(--theme-elevation-150); + background: var(--theme-elevation-50); +} + +.title { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--theme-elevation-800); +} + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + padding: 2px 6px; + border-radius: 6px; + background: var(--theme-elevation-100); + color: var(--theme-elevation-800); + border: 1px solid var(--theme-elevation-150); +} + +.iconButton { + appearance: none; + border: 1px solid var(--theme-elevation-150); + background: var(--theme-elevation-0); + color: var(--theme-elevation-800); + border-radius: 6px; + height: 28px; + width: 28px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.iconButton:hover { + background: var(--theme-elevation-100); +} + +.body { + flex: 1; + overflow: auto; + padding: 12px; +} + +.placeholder { + border: 1px dashed var(--theme-elevation-200); + background: var(--theme-elevation-50); + padding: 12px; + border-radius: 8px; + color: var(--theme-elevation-700); +} + +.placeholderTitle { + margin: 0 0 6px 0; + font-weight: 600; + color: var(--theme-elevation-800); +} + +.placeholderText { + margin: 0; + font-size: 12px; + color: var(--theme-elevation-700); +} + +.footer { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + border-top: 1px solid var(--theme-elevation-150); + background: var(--theme-elevation-50); +} + +.input { + flex: 1; + height: 32px; + border-radius: 8px; + border: 1px solid var(--theme-elevation-150); + padding: 0 10px; + background: var(--theme-elevation-0); + color: var(--theme-elevation-800); +} + +.send { + height: 32px; + padding: 0 12px; + border-radius: 8px; + border: 1px solid var(--theme-elevation-150); + background: var(--theme-elevation-0); + color: var(--theme-elevation-800); + cursor: not-allowed; +} + +.launcher { + position: fixed; + right: calc(var(--agent-nav-width) + 8px); + bottom: 16px; + z-index: 31; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--theme-elevation-150); + background: var(--theme-elevation-0); + color: var(--theme-elevation-800); + box-shadow: 0 2px 10px rgba(0,0,0,0.06); + cursor: pointer; +} + +.launcher:hover { + background: var(--theme-elevation-50); +} From b75754d51feee250996cc8aef38160d00e75c71d Mon Sep 17 00:00:00 2001 From: ashbuilds Date: Thu, 23 Oct 2025 21:11:37 +0200 Subject: [PATCH 2/7] [feat] design agent sider --- dev/dev.db | Bin 421888 -> 421888 bytes src/providers/AgentProvider/AgentProvider.tsx | 21 ++++---- src/ui/AgentSidebar/AgentSidebar.tsx | 27 +++++----- src/ui/AgentSidebar/agent-sidebar.module.css | 46 +++++++----------- src/ui/AgentSidebar/body.css | 6 +++ src/ui/Icons/Icons.tsx | 4 +- src/ui/Icons/icons.module.css | 7 ++- 7 files changed, 55 insertions(+), 56 deletions(-) create mode 100644 src/ui/AgentSidebar/body.css diff --git a/dev/dev.db b/dev/dev.db index e5561d6336b8188b26a2ed7aa029a758682a0517..e99f53c0ec0c3e8b3587d42debb515ff5d602f0f 100644 GIT binary patch delta 905 zcma)*Pe>F|9LL|=nK$F?&hBrG_8>}@Ssk{So%eRfb_N8aOGc*@g6Od7y0Q2#$_^2u zb`VdZ9b#^IC_IEWaR=#O*dZbUK`#czMp1f*Ac8ss+j}ePze~aJ;rD)z-|zSRzV~LL z+%QpYm~F&6kD4ywork%}9G=XDmU7b7R98OR*J~A6-Y7VB!N@SX&FC%kq>Vg}sBQPK z0?jgu*#@-?mhPrmm$kVTH8YgnIFM)7emfLKJV96dl`)d=FMH&4jC!88XO8(Pk93~pA|2P_6={3%j|!*?jKgpRV4{8?J7C5Tv9CCwvnZ<;hk|H(H(;DQ6H(V#Dyc-R22pF=?(DbaDvutm* zPEr?!1%Xo%Lqp|>@o~3&cWk1j+;S^pqxb6vH`4Q_lQ#`jH|?HH_kGLn)JY2oMHKd( z^Hv^^0j*o1If~(o=wY56RWr_XD|&oe)f~qU8YCu*pWzWagV(m-}Pw2oW*bK8PFvw#LQ6^C)A`_koVIpA6(Qf*EnD%3<)AVayb4u9FF27Vu*C_67|*t xl0GKlf1;n{F=gnKp;LxV89HU?l%a>?W#J=;dOrMn4ae^s7ZUQ`)4V1WegnsP!T$gN diff --git a/src/providers/AgentProvider/AgentProvider.tsx b/src/providers/AgentProvider/AgentProvider.tsx index 539e7c8..5a3e766 100644 --- a/src/providers/AgentProvider/AgentProvider.tsx +++ b/src/providers/AgentProvider/AgentProvider.tsx @@ -4,6 +4,7 @@ import React from 'react' import styles from '../../ui/AgentSidebar/agent-sidebar.module.css' import { AgentSidebar } from '../../ui/AgentSidebar/AgentSidebar.js' +import { PluginIcon } from '../../ui/Icons/Icons.js' export const AgentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [open, setOpen] = React.useState(true) @@ -12,17 +13,15 @@ export const AgentProvider: React.FC<{ children: React.ReactNode }> = ({ childre <> {children} - {!open && ( - - )} - + setOpen(false)} open={open} /> ) diff --git a/src/ui/AgentSidebar/AgentSidebar.tsx b/src/ui/AgentSidebar/AgentSidebar.tsx index e340acb..fd2b881 100644 --- a/src/ui/AgentSidebar/AgentSidebar.tsx +++ b/src/ui/AgentSidebar/AgentSidebar.tsx @@ -1,7 +1,8 @@ 'use client' -import React from 'react' +import React, { useEffect } from 'react' +import "./body.css" import styles from './agent-sidebar.module.css' type AgentSidebarProps = { @@ -10,6 +11,15 @@ type AgentSidebarProps = { } export const AgentSidebar: React.FC = ({ onCloseAction, open = false }) => { + useEffect(() => { + const b = document.body + if (!b) return + b.style.setProperty('--agent-sider-w', `340px`) + if (open) b.classList.add('agent-sider-open') + else b.classList.remove('agent-sider-open') + return () => b.classList.remove('agent-sider-open') + }, [open]) + return (
= ({ onCloseAction, open AI Agent
-
-
-

Agent chat

-

- UI placeholder. Streaming and persistence will be added later. -

-
+
diff --git a/src/ui/AgentSidebar/agent-sidebar.module.css b/src/ui/AgentSidebar/agent-sidebar.module.css index 1cedb56..e16ed06 100644 --- a/src/ui/AgentSidebar/agent-sidebar.module.css +++ b/src/ui/AgentSidebar/agent-sidebar.module.css @@ -30,7 +30,6 @@ gap: 8px; padding: 12px 12px; border-bottom: 1px solid var(--theme-elevation-150); - background: var(--theme-elevation-50); } .title { @@ -53,25 +52,6 @@ border: 1px solid var(--theme-elevation-150); } -.iconButton { - appearance: none; - border: 1px solid var(--theme-elevation-150); - background: var(--theme-elevation-0); - color: var(--theme-elevation-800); - border-radius: 6px; - height: 28px; - width: 28px; - line-height: 1; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.iconButton:hover { - background: var(--theme-elevation-100); -} - .body { flex: 1; overflow: auto; @@ -128,20 +108,30 @@ } .launcher { + padding: 8px 5px 5px 5px; + width: 36px; + height: 36px; + border-radius: 50%; + + display: flex; + align-items: center; + justify-content: center; + position: fixed; - right: calc(var(--agent-nav-width) + 8px); - bottom: 16px; + right: 12px; + bottom: 28px; z-index: 31; - display: inline-flex; - align-items: center; gap: 8px; - padding: 8px 12px; - border-radius: 999px; - border: 1px solid var(--theme-elevation-150); - background: var(--theme-elevation-0); + background: var(--color-base-1000); color: var(--theme-elevation-800); + border: 1px solid var(--theme-elevation-100); box-shadow: 0 2px 10px rgba(0,0,0,0.06); cursor: pointer; + transition: right 200ms ease; +} + +.launcherActive { + right: calc(var(--agent-panel-width) + 12px) } .launcher:hover { diff --git a/src/ui/AgentSidebar/body.css b/src/ui/AgentSidebar/body.css new file mode 100644 index 0000000..ecd3198 --- /dev/null +++ b/src/ui/AgentSidebar/body.css @@ -0,0 +1,6 @@ +:root { --agent-sider-w: 340px; } + +.agent-sider-open { + padding-right: var(--agent-sider-w); + transition: padding-right 200ms ease; +} \ No newline at end of file diff --git a/src/ui/Icons/Icons.tsx b/src/ui/Icons/Icons.tsx index 33156fe..67db01d 100644 --- a/src/ui/Icons/Icons.tsx +++ b/src/ui/Icons/Icons.tsx @@ -5,13 +5,15 @@ import LottieAnimation from './LottieAnimation.js' export const PluginIcon = ({ color = 'white', + hasDivider = true, isLoading, }: { color?: string + hasDivider?: boolean isLoading?: boolean }) => { return ( - + ) diff --git a/src/ui/Icons/icons.module.css b/src/ui/Icons/icons.module.css index 15771be..1e9485e 100644 --- a/src/ui/Icons/icons.module.css +++ b/src/ui/Icons/icons.module.css @@ -1,5 +1,5 @@ + .actions_icon { - border-right: 1px solid var(--theme-elevation-150); cursor: pointer; max-width: 30px; padding-right: 8px; @@ -11,6 +11,10 @@ justify-content: center; } +.actions_border { + border-right: 1px solid var(--theme-elevation-150); +} + .icon { display: flex; align-items: center; @@ -24,3 +28,4 @@ .color_fill { fill: var(--theme-elevation-800); } + From a4fae2d0954ca071a0907ebe1da969c01137562f Mon Sep 17 00:00:00 2001 From: ashbuilds Date: Thu, 23 Oct 2025 22:27:31 +0200 Subject: [PATCH 3/7] [feat] AgentInput component --- dev/dev.db | Bin 421888 -> 421888 bytes src/ui/AgentInput/AgentInput.tsx | 99 +++++++++++++++++++ src/ui/AgentInput/agent-input.module.css | 44 +++++++++ src/ui/AgentSidebar/AgentSidebar.tsx | 21 ++-- src/ui/AgentSidebar/agent-sidebar.module.css | 16 ++- 5 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 src/ui/AgentInput/AgentInput.tsx create mode 100644 src/ui/AgentInput/agent-input.module.css diff --git a/dev/dev.db b/dev/dev.db index e99f53c0ec0c3e8b3587d42debb515ff5d602f0f..713be3c16162f5ab2337682f538262301953218c 100644 GIT binary patch delta 66 zcmZp8AldLha)LDD*NHODj9(iQS`!#s6PQ{Pm|GKAwkEKq*K!(JSeY2;nHiaGzfj9s Np@_r}oX@)c0stBn7JL8z delta 66 zcmZp8AldLha)LDD%ZW11j4vA#S`!#s6PQ{Pm|GKAwkEKq*K!(IS{WPb85$aHzfj9s Np@_r}oX@)c0ss@y7E%BJ diff --git a/src/ui/AgentInput/AgentInput.tsx b/src/ui/AgentInput/AgentInput.tsx new file mode 100644 index 0000000..2a45ee1 --- /dev/null +++ b/src/ui/AgentInput/AgentInput.tsx @@ -0,0 +1,99 @@ +'use client' + +import React from 'react' + +import styles from './agent-input.module.css' + +export type AgentInputProps = { + autoFocus?: boolean + className?: string + disabled?: boolean + maxHeight?: number + onSend: (message: string) => void + placeholder?: string +} + +export const AgentInput: React.FC = ({ + autoFocus = false, + className, + disabled = false, + maxHeight = 200, + onSend, + placeholder = 'How can I help you…', +}) => { + const [value, setValue] = React.useState('') + const [isComposing, setIsComposing] = React.useState(false) + const editorRef = React.useRef(null) + + const resize = React.useCallback(() => { + const el = editorRef.current + if (!el) { + return + } + // Reset to auto so scrollHeight measures full content height + el.style.height = 'auto' + const limit = Math.max(0, maxHeight) + const next = Math.min(el.scrollHeight, limit) + el.style.height = `${next}px` + el.style.overflowY = el.scrollHeight > limit ? 'auto' : 'hidden' + }, [maxHeight]) + + React.useEffect(() => { + resize() + }, [value, resize]) + + const send = () => { + if (disabled) { + return + } + const msg = value.trim() + if (!msg) { + return + } + onSend(msg) + setValue('') + // Ensure height resets after clearing + requestAnimationFrame(resize) + } + + const handleKeyDown: React.KeyboardEventHandler = (e) => { + // Shift+Enter = newline (default). Enter alone = send (when not composing). + if (e.key === 'Enter' && !e.shiftKey && !isComposing) { + e.preventDefault() + send() + } + } + + return ( +
+