Repository navigation
feat(mothership): let a question card pick from the workspace's resources #8735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| 'use client' | ||
|
|
||
| import { useMemo } from 'react' | ||
| import { ArrowRight, cn } from '@sim/emcn' | ||
| import { useParams } from 'next/navigation' | ||
| import { INTERACTION_CARD_ROW_CLASSES } from '@/app/workspace/[workspaceId]/home/components/message-content/components/interaction-card' | ||
| import type { QuestionResourceType } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' | ||
| import { resourceFromItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' | ||
| import type { AvailableItem } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/resource-folder-tree' | ||
| import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' | ||
| import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' | ||
| import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge' | ||
| import { useTablesList } from '@/hooks/queries/tables' | ||
| import { useWorkflows } from '@/hooks/queries/workflows' | ||
| import { useWorkspaceFiles } from '@/hooks/queries/workspace-files' | ||
|
|
||
| /** | ||
| * The current workspace's live resources of one family. Only the requested | ||
| * family's list is fetched; the others stay disabled. Outside a workspace (an | ||
| * organization chat) there is nothing to list and the card falls back to its | ||
| * free-text row. | ||
| */ | ||
| function useResourceCandidates(type: QuestionResourceType): { | ||
| candidates: AvailableItem[] | ||
| isPending: boolean | ||
| } { | ||
| const { workspaceId } = useParams<{ workspaceId?: string }>() | ||
| const has = Boolean(workspaceId) | ||
| const workflows = useWorkflows(workspaceId, { enabled: has && type === 'workflow' }) | ||
| const tables = useTablesList(workspaceId, 'active', { enabled: has && type === 'table' }) | ||
| const files = useWorkspaceFiles(workspaceId ?? '', 'active', { enabled: has && type === 'file' }) | ||
| const knowledgeBases = useKnowledgeBasesQuery(workspaceId, { | ||
| enabled: has && type === 'knowledgebase', | ||
| }) | ||
| const query = { workflow: workflows, table: tables, file: files, knowledgebase: knowledgeBases }[ | ||
| type | ||
| ] | ||
| const candidates = useMemo( | ||
| () => (query.data ?? []).map((item) => ({ id: item.id, name: item.name })), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: These workspace-scoped list hooks keep previous data when their keys change, so this renders the prior workspace’s candidates during a workspace switch and allows selecting one in the current chat. Ignore placeholder data and treat it as loading until the current workspace’s list resolves. Prompt for AI agents |
||
| [query.data] | ||
| ) | ||
| return { candidates, isPending: has && query.isPending } | ||
| } | ||
|
|
||
| /** Rows mounted at once; the scroll box shows about five, and typing searches the rest. */ | ||
| const PREVIEW_LIMIT = 50 | ||
|
|
||
| interface ResourceQuestionRowsProps { | ||
| resourceType: QuestionResourceType | ||
| /** The card's free-text entry, which doubles as the list's search filter. */ | ||
| query: string | ||
| disabled: boolean | ||
| onPick: (resource: MothershipResource) => void | ||
| } | ||
|
|
||
| /** | ||
| * Option rows for a `resource_select` question: the workspace's resources of | ||
| * the requested family, rendered with the same row the add-resource menu uses | ||
| * and filtered by what the user types into the card's free-text row. | ||
| */ | ||
| export function ResourceQuestionRows({ | ||
| resourceType, | ||
| query, | ||
| disabled, | ||
| onPick, | ||
| }: ResourceQuestionRowsProps) { | ||
| const { candidates, isPending } = useResourceCandidates(resourceType) | ||
| const config = getResourceConfig(resourceType) | ||
| const needle = query.trim().toLowerCase() | ||
| const matches = useMemo( | ||
| () => | ||
| needle | ||
| ? candidates.filter((candidate) => candidate.name.toLowerCase().includes(needle)) | ||
| : candidates, | ||
| [candidates, needle] | ||
| ) | ||
| const visible = matches.slice(0, PREVIEW_LIMIT) | ||
| const label = config.label.toLowerCase() | ||
|
|
||
| if (visible.length === 0) { | ||
| return ( | ||
| <p className={cn(INTERACTION_CARD_ROW_CLASSES, 'text-[var(--text-muted)] text-sm')}> | ||
| {emptyMessage(label, isPending, needle.length > 0)} | ||
| </p> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div className='max-h-[180px] overflow-y-auto'> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new scroll box hides resource rows without the required edge treatment. The styling guide requires Apply those helpers so users can see when more resources are offscreen. This repository requirement must be satisfied before merging. Context Used: Tailwind CSS and styling conventions (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| {visible.map((candidate, i) => ( | ||
| <button | ||
| key={candidate.id} | ||
| type='button' | ||
| disabled={disabled} | ||
| onClick={() => onPick(resourceFromItem(resourceType, candidate))} | ||
| className={cn( | ||
| INTERACTION_CARD_ROW_CLASSES, | ||
| 'w-full', | ||
| disabled ? 'cursor-not-allowed' : 'hover-hover:bg-[var(--surface-5)]', | ||
| i > 0 && 'border-t' | ||
| )} | ||
| > | ||
| <span className='flex min-w-0 flex-1 items-center gap-2 text-[var(--text-body)] text-sm'> | ||
| {config.renderDropdownItem({ item: candidate })} | ||
| </span> | ||
| <ArrowRight className='size-[16px] shrink-0 text-[var(--text-icon)]' /> | ||
| </button> | ||
| ))} | ||
| {matches.length > visible.length && ( | ||
| <p | ||
| className={cn(INTERACTION_CARD_ROW_CLASSES, 'border-t text-[var(--text-muted)] text-sm')} | ||
| > | ||
| {`${matches.length - visible.length} more ${label} — type to search`} | ||
| </p> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| function emptyMessage(label: string, isPending: boolean, searching: boolean): string { | ||
| if (isPending) return `Loading ${label}…` | ||
| if (searching) return `No matching ${label}` | ||
| return `No ${label} yet` | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Picking a workflow whose name contains a newline breaks the answered recap after reload. Workflow names allow internal newlines, but this passes the name unchanged into an answer format that requires one line per question.
parseQuestionAnswerMessagereturns null, so the chat leaves the question unpaired and shows a separate answer bubble.Normalize line breaks in the picked label before formatting the answer. Keep the resource context unchanged.