From dd9563426217c37037c71e838561a87c134c1446 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 22:43:34 -0700 Subject: [PATCH] FN-7845: show task-scoped artifacts alongside Task Documents Extend the dashboard's Task Documents tab to union each task's registered documents with its task-scoped artifacts, with an inline right-pane viewer for the added artifact types. - DocumentsView Task Documents tab now merges task documents and task-scoped artifacts per task group, sorted/grouped consistently - Adds an inline right-pane artifact viewer (image/video/audio/pdf/inline-doc/other) reusing getArtifactCategory + artifactMediaUrl/fetchArtifact - Selection state is a discriminated document|artifact union kept separate from Project Files selection; the standalone Artifacts gallery tab is unchanged - Updates dashboard-guide.md to describe the merged Task Documents behavior (grouping, search, and preview now cover both documents and artifacts) - Adds a minor changeset for @runfusion/fusion documenting the artifact-in-Task-Documents feature - Expands DocumentsView test coverage for the new union/selection/preview behavior Files changed: .changeset/fn-7845-task-documents-artifacts.md | 7 + docs/dashboard-guide.md | 6 +- .../dashboard/app/components/DocumentsView.css | 142 +++++++- .../dashboard/app/components/DocumentsView.tsx | 389 ++++++++++++++++++--- .../components/__tests__/DocumentsView.test.tsx | 290 ++++++++++++++- 5 files changed, 780 insertions(+), 54 deletions(-) Fusion-Task-Id: FN-7845 Fusion-Task-Lineage: 32bbe6dc-4c02-449c-a13f-38abb8fd727d Co-authored-by: Fusion (runfusion.ai) --- .../fn-7845-task-documents-artifacts.md | 7 + docs/dashboard-guide.md | 6 +- .../app/components/DocumentsView.css | 142 ++++++- .../app/components/DocumentsView.tsx | 389 ++++++++++++++++-- .../__tests__/DocumentsView.test.tsx | 290 ++++++++++++- 5 files changed, 780 insertions(+), 54 deletions(-) create mode 100644 .changeset/fn-7845-task-documents-artifacts.md diff --git a/.changeset/fn-7845-task-documents-artifacts.md b/.changeset/fn-7845-task-documents-artifacts.md new file mode 100644 index 0000000000..b211f0890c --- /dev/null +++ b/.changeset/fn-7845-task-documents-artifacts.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Artifacts view — the Task Documents list now also shows each task's registered artifacts. +category: feature +dev: DocumentsView Task Documents tab unions task documents with task-scoped artifacts per task group and adds an inline right-pane artifact viewer (image/video/audio/pdf/inline-doc/other) reusing getArtifactCategory + artifactMediaUrl/fetchArtifact. Selection is a discriminated document|artifact type kept separate from Project Files selection; the standalone Artifacts gallery tab is unchanged. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 17cbad846b..2d022863ad 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -831,9 +831,9 @@ Artifacts view aggregates registered artifacts, project markdown files, and task Features: -- Browse **Task Documents** in the same left-sidebar/right-pane pattern as **Project Files**: the sidebar groups document entries by task ID in distinct task cards with clear spacing between tasks, revision metadata, and parent task status badges when available, while the right pane loads the selected document content -- Search documents across tasks -- Open project markdown files and task documents with inline preview +- Browse **Task Documents** in the same left-sidebar/right-pane pattern as **Project Files**: the sidebar groups task documents and task-scoped registered artifacts by task ID in distinct task cards with clear spacing between tasks, revision/artifact metadata, and parent task status badges when available, while the right pane loads the selected document or artifact preview +- Search task documents and task-scoped artifacts across tasks +- Open project markdown files, task documents, and task-scoped artifacts with inline preview - Browse the **Artifacts** tab for registry media registered by any agent, dashboard chat/user action, or system tool across tasks - Already-open global and task-detail artifact lists refresh live from the artifact registry event when an agent, dashboard chat session, user action, or system tool registers a new artifact, while preserving active search filters and task scoping - Use the tab-count badges to see the current counts for Project Files, Task Documents, and Artifacts; the Artifacts badge reflects the loaded `GET /api/artifacts` result set, including active search filters diff --git a/packages/dashboard/app/components/DocumentsView.css b/packages/dashboard/app/components/DocumentsView.css index 186c1de259..022dac0d98 100644 --- a/packages/dashboard/app/components/DocumentsView.css +++ b/packages/dashboard/app/components/DocumentsView.css @@ -77,7 +77,7 @@ Artifacts controls are the first page content below the shared header, so add a border-radius: var(--radius-pill); background: color-mix(in srgb, var(--text) 10%, transparent); color: inherit; - font-size: 11px; + font-size: 0.6875rem; font-family: var(--font-mono); } @@ -181,7 +181,7 @@ Artifacts controls are the first page content below the shared header, so add a } .documents-view-empty-hint { - font-size: 13px; + font-size: 0.8125rem; color: var(--text-dim); margin-top: var(--space-xs); } @@ -263,7 +263,7 @@ Artifacts controls are the first page content below the shared header, so add a } .markdown-file-item-name { - font-size: 13px; + font-size: 0.8125rem; font-weight: 600; color: var(--text); } @@ -448,6 +448,138 @@ FN-7836 requires loaded Task Documents to render visible task-card groups even w background: color-mix(in srgb, var(--todo) 14%, var(--card)); } +/* +FNXC:DocumentsView 2026-07-11-22:04: +FN-7845 places registered task artifacts under the same task group as documents, as peer outputs with a small subsection label and icon-bearing rows. Keep these rules scoped to the Task Documents sidebar/right pane so Project Files' shared .markdown-file-item base and the standalone Artifacts gallery remain unchanged. +*/ +.documents-task-artifacts-subsection { + margin: 0; + padding: 0; + list-style: none; + border-top: thin solid var(--border); + background: color-mix(in srgb, var(--surface) 38%, transparent); +} + +.documents-task-artifacts-label { + padding: var(--space-sm) var(--space-lg) var(--space-xs); + color: var(--text-dim); + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.documents-task-artifact-list { + background: transparent; +} + +.documents-task-artifact-item { + background: transparent; +} + +.documents-task-artifact-title-row { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + min-width: 0; + color: var(--text); +} + +.documents-task-artifact-title-row .markdown-file-item-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.documents-task-artifact-viewer { + min-height: 0; +} + +.documents-task-artifact-meta { + align-items: center; +} + +.documents-task-artifact-open-task { + border: 0; + padding: 0; + background: transparent; + color: var(--todo); + cursor: pointer; + font: inherit; +} + +.documents-task-artifact-open-task:hover { + text-decoration: underline; +} + +.documents-task-artifact-open-task:focus-visible { + outline: none; + box-shadow: var(--focus-ring-strong); + border-radius: var(--radius-sm); +} + +.documents-task-artifact-description { + margin: 0; + color: var(--text-muted); + font-size: 0.8125rem; + line-height: 1.5; +} + +.documents-task-artifact-preview { + display: flex; + flex-direction: column; + gap: var(--space-md); + min-height: 0; +} + +.documents-task-artifact-media { + display: block; + width: 100%; + max-height: min(60dvh, 42rem); + object-fit: contain; + border: thin solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); +} + +.documents-task-artifact-audio { + width: 100%; +} + +.documents-task-artifact-pdf { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.documents-task-artifact-pdf iframe { + width: 100%; + min-height: 60dvh; + border: thin solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); +} + +.documents-task-artifact-doc-content { + min-height: 45dvh; +} + +.documents-task-artifact-download { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-md); + padding: var(--space-lg); + border: thin solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + color: var(--text-muted); +} + +.documents-task-artifact-download p { + margin: 0; +} + /* FNXC:DocumentsView 2026-07-11-14:30: The meta line (author · revision · date) renders as its own row directly under the path+actions header row; the viewer column's gap is too tall between two related header rows, so the meta pulls itself closer to the path box it annotates. @@ -536,7 +668,7 @@ The meta line (author · revision · date) renders as its own row directly under .document-mode-toggle { padding: var(--space-xs) var(--space-sm); - font-size: 11px; + font-size: 0.6875rem; font-weight: 500; color: var(--text-muted); background: var(--card); @@ -583,7 +715,7 @@ The meta line (author · revision · date) renders as its own row directly under .document-card-content-text { margin: 0; - font-size: 13px; + font-size: 0.8125rem; color: var(--text); line-height: 1.5; white-space: pre-wrap; diff --git a/packages/dashboard/app/components/DocumentsView.tsx b/packages/dashboard/app/components/DocumentsView.tsx index da390d961f..f5f8ec4c55 100644 --- a/packages/dashboard/app/components/DocumentsView.tsx +++ b/packages/dashboard/app/components/DocumentsView.tsx @@ -1,12 +1,13 @@ import "./DocumentsView.css"; import { useState, useMemo, useCallback, useEffect, useRef, type ChangeEvent } from "react"; import { useTranslation } from "react-i18next"; -import { ArrowLeft, FileText, Pencil, RefreshCw, Search, X, Eye, EyeOff } from "lucide-react"; +import type { TFunction } from "i18next"; +import { ArrowLeft, AudioLines, Download, ExternalLink, FileText, FileType, Image as ImageIcon, Package, Pencil, RefreshCw, Search, Video, X, Eye, EyeOff } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import type { ColumnId, TaskDocumentWithTask, TaskDetail } from "@fusion/core"; +import type { Artifact, ArtifactWithTask, ColumnId, TaskDocumentWithTask, TaskDetail } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; -import { fetchTaskDetail, fetchWorkspaceFileContent, putTaskDocument, saveWorkspaceFileContent, type MarkdownFileEntry } from "../api"; +import { artifactMediaUrl, fetchArtifact, fetchTaskDetail, fetchWorkspaceFileContent, putTaskDocument, saveWorkspaceFileContent, type MarkdownFileEntry } from "../api"; import { useArtifacts } from "../hooks/useArtifacts"; import { useDocuments } from "../hooks/useDocuments"; import { useProjectMarkdownFiles } from "../hooks/useProjectMarkdownFiles"; @@ -14,13 +15,32 @@ import { useSelectionComment } from "../hooks/useSelectionComment"; import { SelectionCommentPopover } from "./SelectionCommentPopover"; import { FileEditor } from "./FileEditor"; import { LoadingSpinner } from "./LoadingSpinner"; -import { ArtifactsGallery } from "./ArtifactsGallery"; +import { ArtifactsGallery, getArtifactCategory, type ArtifactCategory } from "./ArtifactsGallery"; import { ViewHeader } from "./ViewHeader"; import { useColumnLabel } from "../i18n/labels"; const MOBILE_BREAKPOINT = 768; type DocumentsTab = "project" | "tasks" | "artifacts"; +type SelectedTaskItem = { kind: "document" | "artifact"; id: string }; + +type GroupedTaskItems = { + taskId: string; + taskTitle?: string; + taskColumn?: string; + documents: TaskDocumentWithTask[]; + artifacts: ArtifactWithTask[]; + latestUpdated: string; +}; + +const TASK_ARTIFACT_CATEGORY_ICONS: Record = { + image: ImageIcon, + doc: FileText, + pdf: FileType, + video: Video, + audio: AudioLines, + other: Package, +}; export interface DocumentsViewProps { projectId?: string; @@ -54,6 +74,145 @@ function getTaskColumnStatusDotClass(taskColumn: string): string { return "status-dot status-dot--connecting"; } +function getTaskArtifactCategoryLabel(t: TFunction<"app">, category: ArtifactCategory): string { + switch (category) { + case "image": return t("documents.artifactCategoryImage", "Image"); + case "doc": return t("documents.artifactCategoryDoc", "Document"); + case "pdf": return t("documents.artifactCategoryPdf", "PDF"); + case "video": return t("documents.artifactCategoryVideo", "Video"); + case "audio": return t("documents.artifactCategoryAudioSingle", "Audio"); + case "other": return t("documents.artifactCategoryOtherSingle", "Other"); + } +} + +function artifactMatchesTaskSearch(artifact: ArtifactWithTask, normalizedQuery: string): boolean { + if (!normalizedQuery) return true; + return [artifact.title, artifact.description, artifact.taskId, artifact.taskTitle, artifact.mimeType, artifact.authorId] + .some((value) => value?.toLowerCase().includes(normalizedQuery)); +} + +function artifactHasInlineText(artifact: Pick): boolean { + const mime = artifact.mimeType?.toLowerCase().split(";", 1)[0] ?? ""; + return Boolean(artifact.content) || artifact.type === "document" || mime.startsWith("text/") || mime === "application/json" || mime === "application/xml"; +} + +interface TaskArtifactInlineViewerProps { + artifact: ArtifactWithTask; + projectId?: string; + content: string | null; + loading: boolean; + error: string | null; + renderMarkdown: boolean; + onToggleMarkdown: () => void; + onOpenTask: (taskId: string) => void; + t: TFunction<"app">; +} + +function TaskArtifactInlineViewer({ artifact, projectId, content, loading, error, renderMarkdown, onToggleMarkdown, onOpenTask, t }: TaskArtifactInlineViewerProps) { + const [mediaError, setMediaError] = useState(null); + const category = getArtifactCategory(artifact); + const categoryLabel = getTaskArtifactCategoryLabel(t, category); + const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact"); + const mediaUrl = artifactMediaUrl(artifact.id, projectId); + const hasInlineText = category === "doc" && artifactHasInlineText(artifact); + + useEffect(() => { + setMediaError(null); + }, [artifact.id]); + + const mediaErrorNode = mediaError ?

{mediaError}

: null; + + const body = (() => { + if (category === "image") { + return <>{mediaErrorNode}{title} setMediaError(t("documents.artifactMediaFailed", "Failed to load artifact preview."))} />; + } + if (category === "video") { + return <>{mediaErrorNode}