diff --git a/frontend/apps/app/components/Chat/Chat.tsx b/frontend/apps/app/components/Chat/Chat.tsx index 820bad4a62..8aaa1e2b91 100644 --- a/frontend/apps/app/components/Chat/Chat.tsx +++ b/frontend/apps/app/components/Chat/Chat.tsx @@ -23,9 +23,17 @@ interface Props { schemaData: Schema tableGroups?: Record designSession: DesignSession + initialMessage?: string + onSendMessage: (message: string) => void } -export const Chat: FC = ({ schemaData, tableGroups, designSession }) => { +export const Chat: FC = ({ + schemaData, + tableGroups, + designSession, + initialMessage, + onSendMessage, +}) => { const [currentUserId, setCurrentUserId] = useState(null) const { messages, addOrUpdateMessage } = useRealtimeMessages( designSession, @@ -100,6 +108,7 @@ export const Chat: FC = ({ schemaData, tableGroups, designSession }) => { isGenerating: false, // Explicitly set to false for consistency } addOrUpdateMessage(userMessage) + onSendMessage(content) startTransition(() => { startAIResponse(content) @@ -141,6 +150,7 @@ export const Chat: FC = ({ schemaData, tableGroups, designSession }) => { onSendMessage={handleSendMessage} isLoading={isLoading} schema={schemaData} + initialMessage={initialMessage} /> ) diff --git a/frontend/apps/app/components/ChatInput/ChatInput.tsx b/frontend/apps/app/components/ChatInput/ChatInput.tsx index d9d751b55c..8b9d0d3a37 100644 --- a/frontend/apps/app/components/ChatInput/ChatInput.tsx +++ b/frontend/apps/app/components/ChatInput/ChatInput.tsx @@ -112,6 +112,7 @@ export const ChatInput: FC = ({ } else if (hasContent) { // If not loading and has content, send message onSendMessage(message) + setMessage('') setTimeout(() => { const textarea = textareaRef.current @@ -144,8 +145,7 @@ export const ChatInput: FC = ({ setIsMentionSuggestorOpen(false) }, []) - // Adjust height on initial render - useEffect(() => { + const handleAdjustTextareaHeight = useCallback(() => { const textarea = textareaRef.current if (textarea) { textarea.style.height = 'auto' @@ -153,6 +153,20 @@ export const ChatInput: FC = ({ } }, []) + useEffect(() => { + if (initialMessage) { + setMessage(initialMessage) + setTimeout(() => { + handleAdjustTextareaHeight() + }, 0) + } + }, [initialMessage, handleAdjustTextareaHeight]) + + // Adjust height on initial render + useEffect(() => { + handleAdjustTextareaHeight() + }, [handleAdjustTextareaHeight]) + return (
= ({ designSession }) => { const [schema, setSchema] = useState(null) const [isLoadingSchema, startTransition] = useTransition() + const [quickFixMessage, setQuickFixMessage] = useState('') const designSessionId = designSession.id + const handleQuickFix = useCallback((comment: string) => { + const fixMessage = `以下のQA Agentからの指摘を修正してください: + +"${comment}" + +この問題を解決するための具体的な修正案を提案してください。` + setQuickFixMessage(fixMessage) + }, []) + + const handleMessageSent = useCallback(() => { + setQuickFixMessage('') + }, []) + // Load initial schema data useEffect(() => { const loadInitialSchema = async () => { @@ -116,9 +130,16 @@ export const SessionDetailPage: FC = ({ designSession }) => {
- + +
+
+
-
) diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.module.css b/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.module.css deleted file mode 100644 index 330de16182..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.module.css +++ /dev/null @@ -1,58 +0,0 @@ -.wrapper { - display: grid; - grid-template-rows: auto 1fr; - height: 100%; - min-height: 0; -} - -.body { - display: grid; - grid-template-rows: auto 1fr; - gap: var(--spacing-8); - padding: var(--spacing-4) var(--spacing-6); - background: var(--global-background); - color: var(--global-foreground); - overflow-y: scroll; -} - -.tabsRoot { - display: flex; - flex-direction: column; - gap: var(--spacing-3); - min-height: 0; - height: 460px; - padding: var(--spacing-3); - border-radius: var(--border-radius-base); - background: var(--pane-background); - border: var(--border-width-base) solid var(--global-border); -} - -.tabsList { - display: flex; - align-items: center; - gap: var(--spacing-2); -} - -.tabsTrigger { - display: flex; - align-items: center; - justify-content: center; - height: 1.75rem; - padding: 0 var(--spacing-2); - border-radius: var(--border-radius-base); - font-size: var(--font-size-4); - border: solid 1px var(--pane-border); -} - -.tabsTrigger[data-state='active'] { - background-color: var(--button-background-active); -} - -.tabsTrigger:hover { - background-color: var(--button-background-hover); -} - -.tabsContent { - min-height: 0; - overflow-y: scroll; -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.tsx deleted file mode 100644 index 525ee1c1fe..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/Artifact.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { TabsContent, TabsList, TabsRoot, TabsTrigger } from '@/components' -import type { Schema } from '@liam-hq/db-structure' -import type { FC } from 'react' -import styles from './Artifact.module.css' -import { BRDList } from './components/BRDList' -import { ERD } from './components/ERD' -import { Header } from './components/Header' -import { MigrationsViewer } from './components/MigrationsViewer' -import { BRD_LIST, MIGRATIONS_DOC, REVIEW_COMMENTS } from './mock' - -type Props = { - schema: Schema -} - -export const Artifact: FC = ({ schema }) => { - return ( -
-
-
- - - - ERD - - - Migrations - - - - - - - - - - -
-
- ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.module.css b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.module.css deleted file mode 100644 index ca1c0250d7..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.section { - border-radius: var(--border-radius-base); - border: solid 1px var(--global-border); - overflow: hidden; -} - -.sectionTitle { - display: flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-4); - border-bottom: solid 1px var(--global-border); - background: var(--pane-background); - font-size: var(--font-size-5); - font-weight: 600; - color: var(--primary-accent); -} - -.sectionId { - padding: var(--spacing-1) var(--spacing-2); - border-radius: var(--border-radius-base); - background: var(--primary-overlay-20); - font-size: var(--font-size-2); - color: var(--primary-accent); -} - -.sectionBody { - display: flex; - flex-direction: column; - gap: var(--spacing-6); - padding: var(--spacing-4); -} - -.overviewList { - padding: var(--spacing-5) var(--spacing-8); - border-radius: var(--border-radius-md); - background: var(--global-muted-background); - border-left: var(--spacing-1) solid var(--primary-accent); - list-style: square; -} - -.overviewItem { - color: var(--global-body-text); - font-size: var(--font-size-3); - line-height: 1.6; -} - -.subSection { - display: grid; - grid-auto-rows: auto; - gap: var(--spacing-4); -} - -.subSectionTitle { - font-size: var(--font-size-4); - font-weight: 600; - color: var(--global-foreground); -} - -.relatedSchemaWrapper { - height: 400px; -} - -.useCaseList { - display: flex; - flex-direction: column; - gap: var(--spacing-3); -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.tsx deleted file mode 100644 index cf6bbadf5c..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/BRDItem.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { FC } from 'react' -import { ERD } from '../../ERD' -import type { BusinessRequirement } from '../types' -import styles from './BRDItem.module.css' -import { UseCaseItem } from './UseCaseItem' - -type Props = { - item: BusinessRequirement -} - -export const BRDItem: FC = ({ item }) => { - return ( -
-

- {item.id} - {item.name} -

-
-
    - {item.overview.map((text, textIndex) => ( -
  • - {text} -
  • - ))} -
- -
-

Related Tables

-
- -
-
- -
-

Use Cases

-
- {item.useCases.map((useCase) => ( - - ))} -
-
-
-
- ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/index.ts deleted file mode 100644 index 038f604794..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ExecutableDMLBlock' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.module.css b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.module.css deleted file mode 100644 index 434afa2764..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.module.css +++ /dev/null @@ -1,74 +0,0 @@ -.wrapper { - border-radius: var(--border-radius-md); - border: solid 1px var(--global-border); - overflow: hidden; -} - -.title { - display: flex; - align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-4); - background-color: var(--pane-background); - border-bottom: solid 1px var(--global-border); - font-size: var(--font-size-4); - font-weight: 600; - color: var(--global-foreground); -} - -.id { - padding: var(--spacing-1) var(--spacing-2); - border-radius: var(--border-radius-base); - background-color: var(--severity-question-20); - font-size: var(--font-size-2); - font-weight: 500; - color: var(--severity-question-100); -} - -.body { - padding: var(--spacing-4); -} - -.stepList { - color: var(--global-foreground); -} - -.stepItem { - display: flex; - flex-direction: column; - gap: var(--spacing-4); - padding: var(--spacing-3) 0; -} - -.orderAndDescription { - display: grid; - grid-template-columns: auto 1fr; - gap: var(--spacing-3); -} - -.stepOrder { - display: flex; - align-items: center; - justify-content: center; - width: 1.5rem; - height: 1.5rem; - border-radius: var(--border-radius-full); - background: var(--severity-question-20); - font-size: var(--font-size-2); - font-weight: 600; - color: var(--severity-question-100); -} - -.stepDescription { - font-size: var(--font-size-5); - line-height: 1.5rem; - white-space: pre-line; - color: var(--global-body-text); -} - -.dmlBlockList { - display: flex; - flex-direction: column; - gap: var(--spacing-3); - padding-left: 2.25rem; -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.tsx deleted file mode 100644 index 90651bc175..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/UseCaseItem.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { FC } from 'react' -import type { UseCase } from '../../types' -import { ExecutableDMLBlock } from './ExecutableDMLBlock' -import styles from './UseCaseItem.module.css' - -type Props = { - item: UseCase -} - -export const UseCaseItem: FC = ({ item }) => { - return ( -
-

- {item.id} - {item.name} -

-
- {item.steps && ( -
    - {item.steps.map((step) => ( -
  1. -
    - {step.order} -

    {step.description}

    -
    - {step.dmlBlocks && step.dmlBlocks.length > 0 && ( -
    - {step.dmlBlocks.map((block) => ( - - ))} -
    - )} -
  2. - ))} -
- )} -
-
- ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/index.ts deleted file mode 100644 index 9ee33c9129..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './UseCaseItem' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/index.ts deleted file mode 100644 index 89553499ef..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BRDItem' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.module.css b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.module.css deleted file mode 100644 index d8ed3511b8..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - display: flex; - flex-direction: column; - gap: var(--spacing-6); -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.tsx deleted file mode 100644 index 38350447a8..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDList.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FC } from 'react' -import { BRDItem } from './BRDItem' -import styles from './BRDList.module.css' -import type { BusinessRequirement } from './types' - -type Props = { - items: BusinessRequirement[] -} - -export const BRDList: FC = ({ items }) => { - return ( -
- {items.map((item) => ( - - ))} -
- ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/index.ts deleted file mode 100644 index 0a0428cc35..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BRDList' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/types.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/types.ts deleted file mode 100644 index 78a7fcae4f..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Schema } from '@liam-hq/db-structure' - -export type DMLBlock = { - name: string - code: string -} - -type Step = { - order: number - description: string - dmlBlocks?: DMLBlock[] -} - -export type UseCase = { - id: string - name: string - steps: Step[] -} - -export type BusinessRequirement = { - id: string - name: string - overview: string[] - relatedSchema: Schema - useCases: UseCase[] -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/ERD/ERD.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/ERD/ERD.tsx deleted file mode 100644 index 3889f9f2ad..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/ERD/ERD.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ERDRenderer } from '@/features' -import { VersionProvider } from '@/providers' -import { versionSchema } from '@/schemas' -import type { Schema } from '@liam-hq/db-structure' -import type { FC } from 'react' -import { parse } from 'valibot' - -const version = parse(versionSchema, { - version: '0.1.0', - gitHash: process.env.NEXT_PUBLIC_GIT_HASH, - envName: process.env.NEXT_PUBLIC_ENV_NAME, - date: process.env.NEXT_PUBLIC_RELEASE_DATE, - displayedOn: 'web', -}) - -type Props = { - schema: Schema -} - -export const ERD: FC = ({ schema }) => { - return ( - - - - ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.module.css b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.module.css deleted file mode 100644 index d7039b85bd..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.wrapper { - display: flex; - align-items: center; - justify-content: flex-end; - gap: var(--spacing-2); - padding: var(--spacing-2) var(--spacing-6); - border-bottom: 1px solid var(--global-border); -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.tsx deleted file mode 100644 index a12d968536..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/Header.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { FC } from 'react' -import { ExportDropdown } from './ExportDropdown' -import styles from './Header.module.css' -import { VersionDropdown } from './VersionDropdown' - -export const Header: FC = () => { - return ( -
- - -
- ) -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/MigrationsViewer.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/MigrationsViewer.tsx deleted file mode 100644 index e47903c66a..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/MigrationsViewer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { FC } from 'react' -import styles from './MigrationsViewer.module.css' -import { type ReviewComment, useMigrationsViewer } from './useMigrationsViewer' - -type Props = { - doc: string - reviewComments: ReviewComment[] -} - -export const MigrationsViewer: FC = ({ doc, reviewComments }) => { - const { ref } = useMigrationsViewer({ - doc, - reviewComments, - }) - - return
-} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx b/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx deleted file mode 100644 index 38f2efcd11..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client' - -import { sql } from '@codemirror/lang-sql' -import { foldGutter, syntaxHighlighting } from '@codemirror/language' -import { lintGutter } from '@codemirror/lint' -import { EditorState, type Extension } from '@codemirror/state' -import { drawSelection, lineNumbers } from '@codemirror/view' -import { EditorView } from 'codemirror' -import { useEffect, useRef, useState } from 'react' -import { commentStateField, setCommentsEffect } from './commentExtension' -import { customTheme, sqlHighlightStyle } from './editorTheme' -import type { ReviewComment } from './types' - -const baseExtensions: Extension[] = [ - lineNumbers(), - foldGutter(), - drawSelection(), - sql(), - lintGutter(), - commentStateField(), - syntaxHighlighting(sqlHighlightStyle), - customTheme, -] - -type Props = { - doc: string - reviewComments?: ReviewComment[] -} - -export const useMigrationsViewer = ({ doc, reviewComments = [] }: Props) => { - const ref = useRef(null) - const [container, setContainer] = useState() - const [view, setView] = useState() - - useEffect(() => { - if (ref.current) { - setContainer(ref.current) - } - }, []) - - useEffect(() => { - if (!view && container) { - const state = EditorState.create({ - doc, - extensions: [...baseExtensions], - }) - const viewCurrent = new EditorView({ - state, - parent: container, - }) - setView(viewCurrent) - } - }, [view, doc, container]) - - useEffect(() => { - if (!view) return - - const effect = setCommentsEffect.of(reviewComments) - view.dispatch({ effects: [effect] }) - }, [reviewComments, view]) - - return { - ref, - } -} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/mock.ts b/frontend/apps/app/components/SessionDetailPage/components/Artifact/mock.ts deleted file mode 100644 index 06cfec33e8..0000000000 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/mock.ts +++ /dev/null @@ -1,473 +0,0 @@ -import type { BusinessRequirement } from './components/BRDList/types' - -export const MIGRATIONS_DOC = ` --- Migrations will appear here as you chat with AI -create table documents ( - id bigint primary key generated always as identity, - title text not null, - parent_id bigint references documents (id) on delete cascade, - created_at timestamp with time zone default now(), - updated_at timestamp with time zone default now(), - is_folder boolean default false -); -create table document_versions ( - id bigint primary key generated always as identity, - document_id bigint references documents (id) on delete cascade, - version_number int not null, - content text, - created_at timestamp with time zone default now(), - unique (document_id, version_number) -); -create table tags ( - id bigint primary key generated always as identity, - name text not null unique -); -create table document_tags ( - document_id bigint references documents (id) on delete cascade, - tag_id bigint references tags (id) on delete cascade, - primary key (document_id, tag_id) -); -` - -export const REVIEW_COMMENTS = [ - { - fromLine: 7, - toLine: 7, - severity: 'High' as const, - message: - '【パフォーマンス】外部キーにインデックスがありません。JOINやWHERE句での検索性能が著しく低下するため、インデックスの作成を強く推奨します。例: CREATE INDEX idx_documents_parent_id ON documents (parent_id);', - }, - { - fromLine: 7, - toLine: 7, - severity: 'Medium' as const, - message: - '【データ安全】`on delete cascade` は、親ドキュメント削除時に意図せず大量の子孫ドキュメントを削除するリスクがあります。安全のため、アプリケーション側で削除処理を制御するか、`on delete restrict`の使用を検討してください。', - }, - { - fromLine: 9, - toLine: 9, - severity: 'Medium' as const, - message: - '【整合性】`updated_at`はレコード作成時にしか設定されません。更新日時を正しく反映するには、`BEFORE UPDATE`トリガーで自動更新する仕組みが必要です。', - }, - { - fromLine: 10, - toLine: 10, - severity: 'Low' as const, - message: - '【整合性】`is_folder`フラグだけでは、「フォルダなのにコンテンツを持つ」といったデータの不整合を防げません。アプリケーション側で厳密な制御を行うか、制約(CHECK)の追加を検討してください。', - }, - { - fromLine: 15, - toLine: 15, - severity: 'High' as const, - message: - '【パフォーマンス】外部キーにインデックスがありません。ドキュメントIDに基づいたバージョン検索の性能が低下するため、インデックスの作成を強く推奨します。例: CREATE INDEX idx_document_versions_document_id ON document_versions (document_id);', - }, - { - fromLine: 24, - toLine: 24, - severity: 'Low' as const, - message: - '【パフォーマンス】`name`カラムの`unique`制約によりインデックスが自動作成されるため、タグ名での検索は効率的です。これは良い設計です。', - }, - { - fromLine: 27, - toLine: 31, - severity: 'High' as const, - message: - '【パフォーマンス】複合主キー `(document_id, tag_id)` は`document_id`での検索には有効ですが、`tag_id`単体での検索性能を向上させるために、`tag_id`カラムにも個別インデックスを作成することを推奨します。例: `CREATE INDEX idx_document_tags_tag_id ON document_tags (tag_id);`', - }, -] - -export const BRD_LIST: BusinessRequirement[] = [ - { - id: 'BRD-001', - name: 'User Registration', - overview: [ - 'A feature that allows new users to create an account necessary to use this service', - 'Users complete account registration by entering required information and agreeing to the terms of service', - ], - relatedSchema: { - tables: { - users: { - name: 'users', - comment: 'User information management table', - columns: { - user_id: { - name: 'user_id', - type: 'uuid', - default: null, - check: null, - primary: true, - unique: false, - notNull: true, - comment: 'Unique identifier for the user', - }, - username: { - name: 'username', - type: 'varchar(255)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Username (nickname)', - }, - email: { - name: 'email', - type: 'varchar(255)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Email address', - }, - }, - indexes: { - idx_email: { - name: 'idx_email', - unique: true, - columns: ['email'], - type: 'btree', - }, - }, - constraints: {}, - }, - countries: { - name: 'countries', - comment: '国・地域情報テーブル', - columns: { - country_code: { - name: 'country_code', - type: 'varchar(3)', - default: null, - check: null, - primary: true, - unique: false, - notNull: true, - comment: 'Country code', - }, - is_serviced: { - name: 'is_serviced', - type: 'boolean', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Service availability flag', - }, - }, - indexes: {}, - constraints: {}, - }, - }, - relationships: {}, - tableGroups: {}, - }, - useCases: [ - { - id: 'UC-001', - name: 'Normal Flow', - steps: [ - { - order: 1, - description: 'User clicks the "Register" button on the site', - }, - { - order: 2, - description: 'Registration form is displayed', - }, - { - order: 3, - description: - 'User enters the following information:\n - Email address\n - Password\n - Username (nickname)\n - Full name\n - Date of birth', - }, - { - order: 4, - description: - 'User checks the checkbox to agree to the "Terms of Service" and "Privacy Policy"', - }, - { - order: 5, - description: 'User clicks the registration button', - dmlBlocks: [ - { - name: 'Email duplication check', - code: `SELECT user_id FROM users WHERE email = '[entered email address]';`, - }, - { - name: 'Username duplication check', - code: `SELECT user_id FROM users WHERE username = '[entered username]';`, - }, - ], - }, - { - order: 6, - description: 'System verifies if the age restriction is met', - }, - { - order: 7, - description: 'System stores user information in the database', - dmlBlocks: [ - { - name: 'Insert user information into database', - code: `INSERT INTO users ( - username, -- Username (nickname) - email, -- Email address - hashed_password, -- Hashed password - salt, -- Salt for password hashing - first_name, -- First name (split from "full name") - last_name, -- Last name (split from "full name") - date_of_birth, -- Date of birth - user_status, -- Initial user status - kyc_status, -- Initial KYC status - allow_promotions, -- Promotional email opt-in flag - registration_ip, -- IP address at registration - created_at, - updated_at -) VALUES ( - '[entered username]', - '[entered email address]', - '[hashed password]', - '[generated salt]', - '[entered first name]', - '[entered last name]', - '[entered date of birth]', - 'PENDING_VERIFICATION', -- or 'ACTIVE' (depending on email verification) - 'NOT_SUBMITTED', - [promotion permission flag value], -- true or false - '[user IP address]', - NOW(), - NOW() -);`, - }, - ], - }, - { - order: 8, - description: 'System displays registration completion message', - }, - { - order: 9, - description: - 'System sends an account activation email to the registered email address', - }, - ], - }, - { - id: 'UC-002', - name: 'Input Error', - steps: [ - { - order: 1, - description: - 'If required fields are left blank, display error messages at the corresponding locations and abort the registration process\nIf invalid formats are entered in any input field, display error messages at the corresponding locations and abort the registration process (invalid email format, password policy violation, etc.)', - }, - ], - }, - { - id: 'UC-003', - name: 'Email Duplication', - steps: [ - { - order: 1, - description: - 'If an already registered email address is entered, display an error message such as "This email address is already in use" and abort the registration process', - dmlBlocks: [ - { - name: 'Check email duplication', - code: `SELECT user_id FROM users WHERE email = '[entered email address]';`, - }, - ], - }, - ], - }, - ], - }, - { - id: 'BRD-002', - name: 'Login', - overview: [ - 'A feature that allows registered users to authenticate using their credentials (email and password) to access the service', - 'After successful authentication, users can access various features within the service', - ], - relatedSchema: { - tables: { - users: { - name: 'users', - comment: 'User information management table', - columns: { - user_id: { - name: 'user_id', - type: 'uuid', - default: null, - check: null, - primary: true, - unique: false, - notNull: true, - comment: 'Unique user identifier', - }, - username: { - name: 'username', - type: 'varchar(255)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Username (nickname)', - }, - email: { - name: 'email', - type: 'varchar(255)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Email address', - }, - }, - indexes: { - idx_email: { - name: 'idx_email', - unique: true, - columns: ['email'], - type: 'btree', - }, - }, - constraints: {}, - }, - user_login_history: { - name: 'user_login_history', - comment: 'User login history table', - columns: { - id: { - name: 'id', - type: 'bigint', - default: null, - check: null, - primary: true, - unique: false, - notNull: true, - comment: 'Login history ID', - }, - user_id: { - name: 'user_id', - type: 'uuid', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'User ID', - }, - login_at: { - name: 'login_at', - type: 'timestamp', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Login date and time', - }, - ip_address: { - name: 'ip_address', - type: 'varchar(45)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'IP address', - }, - login_status: { - name: 'login_status', - type: 'varchar(20)', - default: null, - check: null, - primary: false, - unique: false, - notNull: true, - comment: 'Login status', - }, - }, - indexes: {}, - constraints: {}, - }, - }, - relationships: {}, - tableGroups: {}, - }, - useCases: [ - { - id: 'UC-001d', - name: 'Normal Flow', - steps: [ - { - order: 1, - description: - 'User enters email address (or username) and password in the login form and clicks the "Login" button', - }, - { - order: 2, - description: - 'System compares the entered password with the hashed password stored in the database', - }, - { - order: 3, - description: - "If the password matches and the user status allows login (e.g., 'ACTIVE'), authentication is successful", - dmlBlocks: [ - { - name: 'Retrieve user information (for authentication)', - code: `SELECT - user_id, - username, - hashed_password, - salt, - user_status, - kyc_status, - email_verified_at -FROM users -WHERE email = '[entered email address]' OR username = '[entered email address/username]';`, - }, - ], - }, - { - order: 4, - description: - 'System initiates a user session (such as issuing a session token)', - }, - { - order: 5, - description: 'System records the login history', - dmlBlocks: [ - { - name: 'Recording login history', - code: `INSERT INTO user_login_history ( - user_id, - login_at, - ip_address, - user_agent, - login_status -) VALUES ( - [user_id of successfully authenticated user], - NOW(), - '[user IP address]', - '[user agent]', - 'SUCCESS' -);`, - }, - ], - }, - ], - }, - ], - }, -] diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/Output.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/Output.module.css new file mode 100644 index 0000000000..211e512c65 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/Output.module.css @@ -0,0 +1,16 @@ +.wrapper { + display: grid; + grid-template-rows: auto 1fr; + height: 100%; + border-radius: var(--border-radius-lg); + background-color: var(--pane-background); + border: solid 1px var(--pane-border); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.04), 0 8px 8px -8px + rgba(0, 0, 0, 0.04); +} + +.body { + /* padding: var(--spacing-3) var(--spacing-2); */ + padding-bottom: var(--spacing-3); + overflow-y: scroll; +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/Output.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/Output.tsx new file mode 100644 index 0000000000..fea612aafe --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/Output.tsx @@ -0,0 +1,65 @@ +import { TabsContent, TabsRoot } from '@/components' +import type { FC } from 'react' +import styles from './Output.module.css' +import { Artifact } from './components/Artifact' +import { DBDesign } from './components/DBDesign' +import { Header } from './components/Header' +import { DEFAULT_OUTPUT_TAB, OUTPUT_TABS } from './constants' +import { useOutputUI } from './hooks/useOutputUI' +import { OutputUIProvider } from './providers/OutputUIProvider' + +type Props = { + onQuickFix?: (comment: string) => void +} + +type OutputContentProps = { + onQuickFix?: (comment: string) => void +} + +const OutputContent: FC = ({ onQuickFix }) => { + const { state, versionData } = useOutputUI() + const currentVersionData = versionData[state.selectedVersion] + + // Get previous version's schemaUpdatesDoc + const versionKeys = Object.keys(versionData) + .map(Number) + .sort((a, b) => a - b) + const currentVersionIndex = versionKeys.indexOf(state.selectedVersion) + const prevVersionKey = + currentVersionIndex > 0 ? versionKeys[currentVersionIndex - 1] : null + const prevSchema = + prevVersionKey !== null ? versionData[prevVersionKey].schema : undefined + const prevSchemaUpdatesDoc = + prevVersionKey !== null + ? versionData[prevVersionKey].schemaUpdatesDoc + : undefined + + return ( + +
+
+ + + + + + +
+ + ) +} + +export const Output: FC = ({ onQuickFix }) => { + return ( + + + + ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.module.css new file mode 100644 index 0000000000..e7b98f4cef --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.module.css @@ -0,0 +1,393 @@ +.container { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--spacing-2); + padding: var(--spacing-4); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-3); + padding-bottom: var(--spacing-2); + border-bottom: 1px solid var(--global-border); +} + +.title { + font-size: var(--font-size-5); + font-weight: 600; + color: var(--global-foreground); + margin: 0; +} + +.copyButton { + display: flex; + align-items: center; + gap: var(--spacing-1); +} + +.messageText { + font-family: var(--main-font); + font-weight: 400; + font-size: var(--font-size-4); + line-height: 1.8em; + color: var(--global-body-text); + width: 100%; + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; +} + +/* Exclude code blocks from text wrapping */ +.messageText :global(pre), +.messageText :global(pre code), +.messageText :global(pre > div) { + word-wrap: normal !important; + overflow-wrap: normal !important; + word-break: normal !important; +} + +/* Headings */ +.messageText :global(h1), +.messageText :global(h2), +.messageText :global(h3), +.messageText :global(h4), +.messageText :global(h5), +.messageText :global(h6) { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.25; + color: var(--global-body-text); +} + +/* First element spacing adjustments */ +.messageText :global(*:first-child) { + margin-top: 0; +} + +.messageText :global(h1) { + margin: 0.67em 0; + font-weight: 600; + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid var(--global-border); +} + +.messageText :global(h2) { + font-weight: 600; + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid var(--global-border); +} + +.messageText :global(h3) { + font-weight: 600; + font-size: 1.25em; +} + +.messageText :global(h4) { + font-weight: 600; + font-size: 1em; +} + +.messageText :global(h5) { + font-weight: 600; + font-size: 0.875em; +} + +.messageText :global(h6) { + font-weight: 600; + font-size: 0.85em; + color: var(--global-body-text-subtle); +} + +/* Paragraphs */ +.messageText :global(p) { + margin-top: 0; + margin-bottom: var(--spacing-2); +} + +/* Lists */ +.messageText :global(ul), +.messageText :global(ol) { + margin-top: 0; + margin-bottom: var(--spacing-2); + padding-left: 2em; +} + +.messageText :global(ul) { + list-style-type: disc; +} + +.messageText :global(ol) { + list-style-type: decimal; +} + +.messageText :global(ol ol), +.messageText :global(ul ol) { + list-style-type: lower-roman; +} + +.messageText :global(ul ul), +.messageText :global(ol ul) { + list-style-type: circle; +} + +.messageText :global(ul ul ul), +.messageText :global(ul ol ul), +.messageText :global(ol ul ul), +.messageText :global(ol ol ul) { + list-style-type: square; +} + +.messageText :global(ul ul ol), +.messageText :global(ul ol ol), +.messageText :global(ol ul ol), +.messageText :global(ol ol ol) { + list-style-type: lower-alpha; +} + +.messageText :global(li + li) { + margin-top: 0.25em; +} + +/* Code */ +.messageText :global(code), +.messageText :global(tt) { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--global-foreground-subtle); + border-radius: var(--border-radius-small); + font-family: var(--code-font); +} + +/* Inline code within paragraphs and other elements */ +.messageText :global(p > code), +.messageText :global(li > code), +.messageText :global(h1 > code), +.messageText :global(h2 > code), +.messageText :global(h3 > code), +.messageText :global(h4 > code), +.messageText :global(h5 > code), +.messageText :global(h6 > code), +.messageText :global(a > code), +.messageText :global(em > code), +.messageText :global(strong > code) { + display: inline; + padding: 0.1em 0.3em; + vertical-align: baseline; + color: #e3e3e3; + background-color: var(--overlay-10); + border-radius: 3px; + font-family: var(--code-font, monospace); + font-size: 85%; +} + +.messageText :global(pre) { + margin-bottom: var(--spacing-2); + width: 100%; + max-width: 100%; + position: relative; + overflow-x: auto; + border-radius: var(--border-radius-base); +} + +/* Add a subtle scrollbar for code blocks that overflow */ +.messageText :global(pre::-webkit-scrollbar) { + height: 8px; + background-color: transparent; +} + +.messageText :global(pre::-webkit-scrollbar-thumb) { + background-color: var(--global-border); + border-radius: 4px; +} + +.messageText :global(pre::-webkit-scrollbar-track) { + background-color: transparent; +} + +.messageText :global(pre code) { + display: inline-block; + max-width: 100%; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal !important; + word-break: normal !important; + white-space: pre !important; + background-color: transparent; + border: 0; + font-size: 12px; +} + +/* Ensure SyntaxHighlighter's container displays properly */ +.messageText :global(pre > div) { + white-space: pre !important; + overflow-x: auto !important; + word-wrap: normal !important; + word-break: normal !important; +} + +/* Links */ +.messageText :global(a) { + background-color: transparent; + color: var(--primary-accent); + text-decoration: none; +} + +.messageText :global(a:hover) { + text-decoration: underline; +} + +/* Images */ +.messageText :global(img) { + border-style: none; + max-width: 100%; + box-sizing: content-box; + border-radius: var(--border-radius-base); +} + +/* Blockquotes */ +.messageText :global(blockquote) { + margin: 0 0 var(--spacing-2) 0; + padding: 0 var(--spacing-2); + color: var(--global-body-text-subtle); + border-left: 0.25em solid var(--global-border); +} + +.messageText :global(blockquote) > :global(:first-child) { + margin-top: 0; +} + +.messageText :global(blockquote) > :global(:last-child) { + margin-bottom: 0; +} + +/* Tables */ +.messageText :global(table) { + border-spacing: 0; + border-collapse: separate; + display: block; + width: 100%; + max-width: 100%; + overflow-x: auto; + margin-bottom: var(--spacing-3); + border: 1px solid var(--global-border); + border-radius: var(--border-radius-base); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* Table scrollbar styling */ +.messageText :global(table::-webkit-scrollbar) { + height: 8px; + background-color: transparent; +} + +.messageText :global(table::-webkit-scrollbar-thumb) { + background-color: var(--global-border); + border-radius: 4px; +} + +.messageText :global(table::-webkit-scrollbar-track) { + background-color: transparent; +} + +.messageText :global(table th) { + font-weight: 600; + background-color: var(--global-foreground-subtle); + color: var(--global-body-text); + text-align: left; + position: sticky; + top: 0; + z-index: 1; + border-bottom: 2px solid var(--global-border); +} + +.messageText :global(table th), +.messageText :global(table td) { + padding: var(--spacing-1half) var(--spacing-2); + border: 1px solid var(--global-border); + border-top: none; + border-left: none; +} + +.messageText :global(table th:last-child), +.messageText :global(table td:last-child) { + border-right: none; +} + +.messageText :global(table tr:last-child td) { + border-bottom: none; +} + +.messageText :global(table tr) { + background-color: var(--global-background); + transition: background-color 0.15s ease; +} + +.messageText :global(table tr:nth-child(2n)) { + background-color: rgba(var(--global-foreground-subtle-rgb, 0, 0, 0), 0.3); +} + +.messageText :global(table tr:hover) { + background-color: rgba(var(--primary-accent-rgb, 0, 0, 0), 0.05); +} + +/* Horizontal Rule */ +.messageText :global(hr) { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--global-border); + height: 0.25em; + padding: 0; + margin: var(--spacing-3) 0; + background-color: var(--global-border); + border: 0; +} + +/* Emphasis */ +.messageText :global(strong) { + font-weight: 600; +} + +.messageText :global(em) { + font-style: italic; +} + +/* Strikethrough */ +.messageText :global(del) { + text-decoration: line-through; +} + +/* Details/Summary */ +.messageText :global(details) { + margin-bottom: var(--spacing-2); + border: 1px solid var(--global-border); + border-radius: var(--border-radius-base); + padding: var(--spacing-2); +} + +.messageText :global(summary) { + cursor: pointer; + font-weight: 600; + padding: var(--spacing-1) 0; + color: var(--primary-accent); +} + +.messageText :global(summary:hover) { + color: var(--primary-accent-hover); +} + +.messageText :global(details[open] summary) { + margin-bottom: var(--spacing-2); + border-bottom: 1px solid var(--global-border); +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.tsx new file mode 100644 index 0000000000..b6bb3423b4 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/Artifact.tsx @@ -0,0 +1,115 @@ +'use client' + +import { + Check, + Copy, + IconButton, + syntaxCodeTagProps, + syntaxCustomStyle, + syntaxTheme, +} from '@liam-hq/ui' +import type { FC, HTMLAttributes, ReactNode } from 'react' +import { useState } from 'react' +import ReactMarkdown from 'react-markdown' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import rehypeRaw from 'rehype-raw' +import remarkGfm from 'remark-gfm' +import styles from './Artifact.module.css' +import { type DMLBlock, ExecutableDMLBlock } from './ExecutableDMLBlock' +import { SeverityBadge } from './SeverityBadge' + +interface CodeProps extends HTMLAttributes { + className?: string + children?: ReactNode +} + +type Props = { + content: string +} + +export const Artifact: FC = ({ content }) => { + const [isCopied, setIsCopied] = useState(false) + + const handleCopyMarkdown = async () => { + try { + await navigator.clipboard.writeText(content) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + } catch (error) { + console.error('Failed to copy markdown:', error) + } + } + + return ( +
+
+

Artifact

+ : } + tooltipContent={isCopied ? 'Copied!' : 'Copy Markdown'} + onClick={handleCopyMarkdown} + /> +
+
+ + } + + return

{children}

+ }, + code(props: CodeProps) { + const { children, className, ...rest } = props + const match = /language-(\w+)/.exec(className || '') + const isInline = !match && !className + const language = match?.[1] + + // Use ExecutableDMLBlock for SQL code blocks + if (!isInline && language === 'sql') { + const sqlCode = String(children).replace(/\n$/, '') + const dmlBlock: DMLBlock = { + name: 'SQL Query', + code: sqlCode, + } + return + } + + return !isInline && match ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ) + }, + }} + > + {content} +
+
+
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/ExecutableDMLBlock.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/ExecutableDMLBlock.module.css similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/ExecutableDMLBlock.module.css rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/ExecutableDMLBlock.module.css diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/ExecutableDMLBlock.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/ExecutableDMLBlock.tsx similarity index 96% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/ExecutableDMLBlock.tsx rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/ExecutableDMLBlock.tsx index bb25269ff7..7f4f67f0c1 100644 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/BRDList/BRDItem/UseCaseItem/ExecutableDMLBlock/ExecutableDMLBlock.tsx +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/ExecutableDMLBlock.tsx @@ -5,9 +5,13 @@ import { type FC, useState } from 'react' import { Light as SyntaxHighlighter } from 'react-syntax-highlighter' import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql' import { atomOneDark as base } from 'react-syntax-highlighter/dist/esm/styles/hljs' -import type { DMLBlock } from '../../../types' import styles from './ExecutableDMLBlock.module.css' +export type DMLBlock = { + name: string + code: string +} + SyntaxHighlighter.registerLanguage('sql', sql) const myTheme = { diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/index.ts new file mode 100644 index 0000000000..a2b4d4d1d6 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/ExecutableDMLBlock/index.ts @@ -0,0 +1 @@ +export { ExecutableDMLBlock, type DMLBlock } from './ExecutableDMLBlock' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.module.css new file mode 100644 index 0000000000..31143d3057 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.module.css @@ -0,0 +1,32 @@ +.badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + margin-bottom: var(--spacing-3); + padding: var(--spacing-1) var(--spacing-2); + border-radius: var(--border-radius-base); + font-size: var(--font-size-2); + font-weight: 600; + font-family: var(--main-font); + border: 1px solid; + white-space: nowrap; + line-height: 1; +} + +.high { + background-color: var(--severity-critical-5); + border-color: var(--severity-critical-40); + color: var(--severity-critical-100); +} + +.medium { + background-color: var(--severity-warning-5); + border-color: var(--severity-warning-40); + color: var(--severity-warning-100); +} + +.low { + background-color: var(--severity-positive-5); + border-color: var(--severity-positive-40); + color: var(--severity-positive-100); +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.tsx new file mode 100644 index 0000000000..087404172b --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/SeverityBadge.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react' +import styles from './SeverityBadge.module.css' + +type SeverityLevel = 'High' | 'Medium' | 'Low' + +type Props = { + level: SeverityLevel +} + +const severityClassMap: Record = { + High: styles.high, + Medium: styles.medium, + Low: styles.low, +} + +export const SeverityBadge: FC = ({ level }) => { + return ( +
+ {level} +
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/index.ts new file mode 100644 index 0000000000..f9fa502b67 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/SeverityBadge/index.ts @@ -0,0 +1 @@ +export { SeverityBadge } from './SeverityBadge' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/index.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/index.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Artifact/index.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.module.css new file mode 100644 index 0000000000..48c15d0fb8 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.module.css @@ -0,0 +1,23 @@ +.wrapper { + display: grid; + grid-template-rows: auto 1fr; + gap: var(--spacing-7); + height: 100%; + min-height: 0; + padding: var(--spacing-4) var(--spacing-3); +} + +.section { + display: grid; + grid-template-rows: auto 1fr; + gap: var(--spacing-3); +} + +.erdWrapper { + height: 480px; +} + +.sectionTitle { + font-size: var(--font-size-7); + font-weight: 600; +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.tsx new file mode 100644 index 0000000000..a667b474c5 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/DBDesign.tsx @@ -0,0 +1,36 @@ +import type { Schema } from '@liam-hq/db-structure' +import type { FC } from 'react' +import styles from './DBDesign.module.css' +import { ERD } from './components/ERD' +import { SchemaUpdates } from './components/SchemaUpdates' +import type { ReviewComment } from './components/SchemaUpdates/MigrationsViewer/useMigrationsViewer' + +type Props = { + schema: Schema + prevSchema?: Schema + schemaUpdatesDoc: string + prevSchemaUpdatesDoc?: string + comments: ReviewComment[] + onQuickFix?: (comment: string) => void +} + +export const DBDesign: FC = ({ + schema, + prevSchema, + schemaUpdatesDoc, + prevSchemaUpdatesDoc, + comments, + onQuickFix, +}) => { + return ( +
+ + +
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.module.css new file mode 100644 index 0000000000..8af7c189a5 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.module.css @@ -0,0 +1,26 @@ +.section { + display: grid; + grid-template-rows: auto 1fr; + gap: var(--spacing-3); +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: var(--spacing-1half); + border-bottom: solid 1px var(--pane-border); +} + +.sectionTitle { + font-size: var(--font-size-7); + font-weight: 600; +} + +.active { + color: var(--primary-accent) !important; +} + +.erdWrapper { + height: 480px; +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.tsx new file mode 100644 index 0000000000..2263987229 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/ERD.tsx @@ -0,0 +1,61 @@ +'use client' + +import { IconButton } from '@/components' +import { ERDRenderer } from '@/features' +import { VersionProvider } from '@/providers' +import { versionSchema } from '@/schemas' +import type { Schema } from '@liam-hq/db-structure' +import clsx from 'clsx' +import { FileDiff } from 'lucide-react' +import { type FC, useMemo, useState } from 'react' +import { parse } from 'valibot' +import styles from './ERD.module.css' + +const version = parse(versionSchema, { + version: '0.1.0', + gitHash: process.env.NEXT_PUBLIC_GIT_HASH, + envName: process.env.NEXT_PUBLIC_ENV_NAME, + date: process.env.NEXT_PUBLIC_RELEASE_DATE, + displayedOn: 'web', +}) + +type Props = { + schema: Schema + prevSchema?: Schema +} + +export const ERD: FC = ({ schema, prevSchema }) => { + const [showDiff, setShowDiff] = useState(false) + + const disabled = useMemo(() => { + return !prevSchema + }, [prevSchema]) + + return ( +
+
+

ER Diagram

+ + } + tooltipContent="Diff View" + onClick={() => setShowDiff((prev) => !prev)} + /> +
+
+ + + +
+
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/ERD/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/index.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/ERD/index.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/ERD/index.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/MigrationsViewer.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/MigrationsViewer.module.css similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/MigrationsViewer.module.css rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/MigrationsViewer.module.css diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/MigrationsViewer.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/MigrationsViewer.tsx new file mode 100644 index 0000000000..cb15308238 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/MigrationsViewer.tsx @@ -0,0 +1,32 @@ +import type { FC } from 'react' +import styles from './MigrationsViewer.module.css' +import { type ReviewComment, useMigrationsViewer } from './useMigrationsViewer' + +type Props = { + doc: string + prevDoc?: string + showDiff?: boolean + comments: ReviewComment[] + showComments: boolean + onQuickFix?: (comment: string) => void +} + +export const MigrationsViewer: FC = ({ + doc, + prevDoc, + showDiff, + comments, + showComments, + onQuickFix, +}) => { + const { ref } = useMigrationsViewer({ + doc, + prevDoc, + showDiff, + comments, + showComments, + onQuickFix, + }) + + return
+} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/index.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/index.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/index.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.module.css new file mode 100644 index 0000000000..0b8c05101e --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.module.css @@ -0,0 +1,62 @@ +.wrapper { + padding: var(--spacing-1) var(--spacing-3); +} + +.container { + padding: var(--spacing-4); + border-radius: var(--border-radius-md); + border: solid 1px var(--pane-border); + background-color: var(--pane-background); +} + +.content { + display: grid; + grid-template-rows: auto 1fr; + place-items: flex-start; + gap: var(--spacing-4); +} + +.commenter { + font-size: var(--font-size-4); + font-weight: 700; + line-height: 1; +} + +.badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + padding: var(--spacing-1) var(--spacing-2); + border-radius: var(--border-radius-base); + font-size: var(--font-size-2); + font-weight: 600; + font-family: var(--main-font); + border: 1px solid; + white-space: nowrap; + line-height: 1; +} + +.high { + background-color: var(--severity-critical-5); + border-color: var(--severity-critical-40); + color: var(--severity-critical-100); +} + +.medium { + background-color: var(--severity-warning-5); + border-color: var(--severity-warning-40); + color: var(--severity-warning-100); +} + +.low { + background-color: var(--severity-positive-5); + border-color: var(--severity-positive-40); + color: var(--severity-positive-100); +} + +.text { + font-size: var(--font-size-5); + white-space: pre-wrap; + word-wrap: break-word; + box-sizing: border-box; +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.tsx new file mode 100644 index 0000000000..ea4a128a84 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/Comment.tsx @@ -0,0 +1,44 @@ +import { Button } from '@liam-hq/ui' +import type { FC } from 'react' +import styles from './Comment.module.css' + +type SeverityLevel = 'High' | 'Medium' | 'Low' + +type Props = { + level: SeverityLevel + comment: string + onQuickFix?: (comment: string) => void +} + +const severityClassMap: Record = { + High: styles.high, + Medium: styles.medium, + Low: styles.low, +} + +export const Comment: FC = ({ level, comment, onQuickFix }) => { + const handleQuickFix = () => { + onQuickFix?.(comment) + } + return ( +
+
+
+
+ {level} +
+

{comment}

+ {onQuickFix && ( + + )} +
+
+
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/index.ts new file mode 100644 index 0000000000..85a5c82013 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/Comment/index.ts @@ -0,0 +1 @@ +export * from './Comment' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/commentExtension.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/commentExtension.tsx similarity index 67% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/commentExtension.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/commentExtension.tsx index e8de414fa5..d8904e5abc 100644 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/commentExtension.ts +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/commentExtension.tsx @@ -6,19 +6,31 @@ import { EditorView, WidgetType, } from '@codemirror/view' +import { createRoot } from 'react-dom/client' +import { Comment } from './Comment' import type { ReviewComment } from './types' // Widget that displays comments as DOM elements class CommentWidget extends WidgetType { - constructor(readonly comment: ReviewComment) { + constructor( + readonly comment: ReviewComment, + readonly onQuickFix: (comment: string) => void, + ) { super() } toDOM() { - const wrap = document.createElement('div') - wrap.className = `cm-comment-widget severity-${this.comment.severity.toLowerCase()}` - wrap.textContent = this.comment.message - return wrap + const container = document.createElement('div') + const root = createRoot(container) + root.render( + , + ) + + return container } ignoreEvent() { @@ -44,29 +56,37 @@ const createLineDecorations = (comment: ReviewComment, doc: Text) => { } // Helper function to create widget decoration for a comment -const createWidgetDecoration = (comment: ReviewComment, doc: Text) => { +const createWidgetDecoration = ( + comment: ReviewComment, + doc: Text, + onQuickFix: (comment: string) => void, +) => { const widgetLine = doc.line(comment.toLine) return Decoration.widget({ - widget: new CommentWidget(comment), + widget: new CommentWidget(comment, onQuickFix), side: 1, }).range(widgetLine.to) } // Helper function to create all decorations for a comment -const createCommentDecorations = (comment: ReviewComment, doc: Text) => { +const createCommentDecorations = ( + comment: ReviewComment, + doc: Text, + onQuickFix: (comment: string) => void, +) => { if (comment.toLine > doc.lines) { return [] } const lineDecorations = createLineDecorations(comment, doc) - const widgetDecoration = createWidgetDecoration(comment, doc) + const widgetDecoration = createWidgetDecoration(comment, doc, onQuickFix) return [...lineDecorations, widgetDecoration] } export const setCommentsEffect = StateEffect.define() -export const commentStateField = () => { +export const commentStateField = (onQuickFix: (comment: string) => void) => { return StateField.define({ create() { return Decoration.none @@ -80,7 +100,7 @@ export const commentStateField = () => { } const newDecorations = comments.flatMap((comment) => - createCommentDecorations(comment, tr.state.doc), + createCommentDecorations(comment, tr.state.doc, onQuickFix), ) return Decoration.set(newDecorations, true) diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/editorTheme.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/editorTheme.ts similarity index 70% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/editorTheme.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/editorTheme.ts index 9220c41b49..540f68b3f5 100644 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/editorTheme.ts +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/editorTheme.ts @@ -10,6 +10,10 @@ export const customTheme = EditorView.theme({ }, '.cm-lineNumbers': { color: 'var(--overlay-20)' }, '.cm-foldGutter': { color: 'var(--overlay-50)' }, + '.cm-content': { + flex: 1, + background: 'var(--global-background)', + }, '.cm-selectionBackground': { background: 'linear-gradient(0deg, var(--color-green-alpha-20, rgba(29,237,131,.20)) 0%, var(--color-green-alpha-20, rgba(29,237,131,.20)) 100%), var(--global-background,#141616) !important', @@ -19,42 +23,9 @@ export const customTheme = EditorView.theme({ animation: 'slow-blink 1s steps(2,start) infinite', }, '@keyframes slow-blink': { to: { visibility: 'hidden' } }, - - // Styles for review comments - '.severity-bg-high': { - backgroundColor: 'rgba(239, 68, 68, 0.1)', - }, - '.severity-bg-medium': { - backgroundColor: 'rgba(245, 158, 11, 0.1)', - }, - '.severity-bg-low': { - backgroundColor: 'rgba(99, 241, 120, 0.1)', - }, - '.cm-comment-widget': { - padding: '8px 12px', - marginLeft: '30px', - borderLeft: '3px solid', - marginTop: '4px', - borderRadius: '0 4px 4px 0', - }, - '.severity-high': { - backgroundColor: '#fee2e2', - borderColor: '#ef4444', - color: '#333', - }, - '.severity-medium': { - backgroundColor: '#fffbe6', - borderColor: '#f59e0b', - color: '#333', - }, - '.severity-low': { - backgroundColor: '#eefff4', - borderColor: '#63f187', - color: '#333', - }, }) -// SQLシンタックスハイライトのスタイル +// SQL syntax highlighting styles export const sqlHighlightStyle = HighlightStyle.define([ // DDL Keywords (CREATE, ALTER, DROP, TABLE, etc.) { tag: tags.keyword, color: '#FF6B9D', fontWeight: 'bold' }, diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/index.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/index.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/index.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/types.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/types.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/MigrationsViewer/useMigrationsViewer/types.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/types.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx new file mode 100644 index 0000000000..04b0704e71 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer/useMigrationsViewer.tsx @@ -0,0 +1,147 @@ +'use client' + +import { sql } from '@codemirror/lang-sql' +import { foldGutter, syntaxHighlighting } from '@codemirror/language' +import { lintGutter } from '@codemirror/lint' +import { unifiedMergeView } from '@codemirror/merge' +import { EditorState, type Extension } from '@codemirror/state' +import { drawSelection, lineNumbers } from '@codemirror/view' +import { EditorView } from 'codemirror' +import { useEffect, useRef, useState } from 'react' +import { commentStateField, setCommentsEffect } from './commentExtension' +import { customTheme, sqlHighlightStyle } from './editorTheme' +import type { ReviewComment } from './types' + +const baseExtensions: Extension[] = [ + lineNumbers(), + foldGutter(), + drawSelection(), + sql(), + lintGutter(), + syntaxHighlighting(sqlHighlightStyle), + customTheme, +] + +type Props = { + doc: string + prevDoc?: string + showDiff?: boolean + comments?: ReviewComment[] + showComments?: boolean + onQuickFix?: (comment: string) => void +} + +export const useMigrationsViewer = ({ + doc, + prevDoc, + showDiff = false, + comments = [], + showComments = false, + onQuickFix, +}: Props) => { + const ref = useRef(null) + const [container, setContainer] = useState() + const [view, setView] = useState() + + useEffect(() => { + if (ref.current) { + setContainer(ref.current) + } + }, []) + + const buildExtensions = ( + showComments: boolean, + onQuickFix?: (comment: string) => void, + showDiff?: boolean, + prevDoc?: string, + ): Extension[] => { + const extensions = [...baseExtensions] + + if (showComments && onQuickFix) { + extensions.push(commentStateField(onQuickFix)) + } + + if (showDiff && prevDoc) { + extensions.push( + ...unifiedMergeView({ + original: prevDoc, + highlightChanges: true, + gutter: true, + mergeControls: false, + syntaxHighlightDeletions: true, + allowInlineDiffs: true, + }), + ) + } + + return extensions + } + + const createEditorView = ( + doc: string, + extensions: Extension[], + container: HTMLDivElement, + ): EditorView => { + const state = EditorState.create({ + doc, + extensions, + }) + + return new EditorView({ + state, + parent: container, + }) + } + + const applyComments = ( + view: EditorView, + showComments: boolean, + comments: ReviewComment[], + ): void => { + if (showComments && comments.length > 0) { + const commentEffect = setCommentsEffect.of(comments) + view.dispatch({ effects: [commentEffect] }) + } + } + + // biome-ignore lint/correctness/useExhaustiveDependencies: including view in the dependency array causes an infinite loop + useEffect(() => { + if (!container) return + + // Clean up existing view + if (view) { + view.destroy() + setView(undefined) + } + + const extensions = buildExtensions( + showComments, + onQuickFix, + showDiff, + prevDoc, + ) + const viewCurrent = createEditorView(doc, extensions, container) + setView(viewCurrent) + + applyComments(viewCurrent, showComments, comments) + }, [doc, prevDoc, showDiff, container, showComments, comments]) + + useEffect(() => { + if (!view || !showComments) return + + const effect = setCommentsEffect.of(comments) + view.dispatch({ effects: [effect] }) + }, [comments, view, showComments]) + + useEffect(() => { + return () => { + if (view) { + view.destroy() + } + } + }, [view]) + + return { + ref, + } +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.module.css new file mode 100644 index 0000000000..b88c55d47f --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.module.css @@ -0,0 +1,28 @@ +.section { + display: grid; + grid-template-rows: auto 1fr; + gap: var(--spacing-3); +} + +.head { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: var(--spacing-1half); + border-bottom: solid 1px var(--pane-border); +} + +.sectionTitle { + font-size: var(--font-size-7); + font-weight: 600; +} + +.controls { + display: flex; + align-items: center; + gap: var(--spacing-1half); +} + +.active { + color: var(--primary-accent) !important; +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.tsx new file mode 100644 index 0000000000..6f5aef52a6 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/SchemaUpdates.tsx @@ -0,0 +1,67 @@ +'use client' + +import { IconButton } from '@/components' +import clsx from 'clsx' +import { FileDiff, MessageSquareCode } from 'lucide-react' +import { type FC, useMemo, useState } from 'react' +import { MigrationsViewer } from './MigrationsViewer' +import type { ReviewComment } from './MigrationsViewer/useMigrationsViewer' +import styles from './SchemaUpdates.module.css' + +type Props = { + schemaUpdatesDoc: string + prevSchemaUpdatesDoc?: string + comments: ReviewComment[] + onQuickFix?: (comment: string) => void +} + +export const SchemaUpdates: FC = ({ + schemaUpdatesDoc, + prevSchemaUpdatesDoc, + comments, + onQuickFix, +}) => { + const [showDiff, setShowDiff] = useState(false) + const [showReviewComments, setShowReviewComments] = useState(false) + + const disabledShowDiff = useMemo(() => { + return !prevSchemaUpdatesDoc + }, [prevSchemaUpdatesDoc]) + + return ( +
+
+

Schema Updates

+
+ + } + tooltipContent="Diff View" + onClick={() => setShowDiff((prev) => !prev)} + /> + + } + tooltipContent="Migration Review" + onClick={() => setShowReviewComments((prev) => !prev)} + /> +
+
+ +
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/index.ts new file mode 100644 index 0000000000..e83ad6789d --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/components/SchemaUpdates/index.ts @@ -0,0 +1 @@ +export * from './SchemaUpdates' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/index.ts new file mode 100644 index 0000000000..adeb06ff98 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/DBDesign/index.ts @@ -0,0 +1 @@ +export * from './DBDesign' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/ExportDropdown.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/ExportDropdown.tsx similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/ExportDropdown.tsx rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/ExportDropdown.tsx diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.module.css b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.module.css new file mode 100644 index 0000000000..5e43e2af00 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.module.css @@ -0,0 +1,39 @@ +.wrapper { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-2); + padding: var(--spacing-2); + border-bottom: 1px solid var(--global-border); +} + +.tabsList { + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +.tabsTrigger { + display: flex; + align-items: center; + justify-content: center; + height: 1.75rem; + padding: 0 var(--spacing-2); + border-radius: var(--border-radius-base); + font-size: var(--font-size-4); + border: solid 1px var(--pane-border); +} + +.tabsTrigger[data-state='active'] { + background-color: var(--button-background-active); +} + +.tabsTrigger:hover { + background-color: var(--button-background-hover); +} + +.tail { + display: flex; + align-items: center; + gap: var(--spacing-2); +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.tsx new file mode 100644 index 0000000000..84df9a3c1d --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/Header.tsx @@ -0,0 +1,28 @@ +import { TabsList, TabsTrigger } from '@/components' +import type { FC } from 'react' +import { OUTPUT_TABS_LIST } from '../../constants' +import { ExportDropdown } from './ExportDropdown' +import styles from './Header.module.css' +import { VersionDropdown } from './VersionDropdown' + +export const Header: FC = () => { + return ( +
+ + {OUTPUT_TABS_LIST.map((tab) => ( + + {tab.label} + + ))} + +
+ + +
+
+ ) +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/VersionDropdown.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/VersionDropdown.tsx similarity index 60% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/VersionDropdown.tsx rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/VersionDropdown.tsx index 868fa2ff1a..56cc5f009c 100644 --- a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/VersionDropdown.tsx +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/VersionDropdown.tsx @@ -9,25 +9,17 @@ import { DropdownMenuRoot, DropdownMenuTrigger, } from '@liam-hq/ui' -import { type FC, useState } from 'react' - -type Version = { - id: string - label: string -} - -const versions: Version[] = [ - { id: 'v0', label: 'v0' }, - { id: 'v1', label: 'v1' }, - { id: 'v2', label: 'v2' }, -] +import type { FC } from 'react' +import { useOutputUI } from '../../hooks/useOutputUI' +import { AVAILABLE_VERSIONS } from '../../mock/versionData' export const VersionDropdown: FC = () => { - const [selectedVersion, setSelectedVersion] = useState(versions[0]) + const { state, actions } = useOutputUI() + const { selectedVersion } = state + const { setSelectedVersion } = actions - const handleVersionSelect = (version: Version) => { + const handleVersionSelect = (version: number) => { setSelectedVersion(version) - // TODO: Implement version switching functionality } return ( @@ -38,17 +30,17 @@ export const VersionDropdown: FC = () => { size="sm" rightIcon={} > - {selectedVersion.label} + v{selectedVersion} - {versions.map((version) => ( + {AVAILABLE_VERSIONS.map((version) => ( handleVersionSelect(version)} > - {version.label} + v{version} ))} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/index.ts similarity index 100% rename from frontend/apps/app/components/SessionDetailPage/components/Artifact/components/Header/index.ts rename to frontend/apps/app/components/SessionDetailPage/components/Output/components/Header/index.ts diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/constants.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/constants.ts new file mode 100644 index 0000000000..918dbd6eb2 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/constants.ts @@ -0,0 +1,18 @@ +export const OUTPUT_TABS = { + DB_DESIGN: 'db-design', + ARTIFACT: 'artifact', +} as const + +type OutputTabValue = (typeof OUTPUT_TABS)[keyof typeof OUTPUT_TABS] + +type OutputTab = { + value: OutputTabValue + label: string +} + +export const OUTPUT_TABS_LIST: OutputTab[] = [ + { value: OUTPUT_TABS.DB_DESIGN, label: 'DB Design' }, + { value: OUTPUT_TABS.ARTIFACT, label: 'Artifact' }, +] + +export const DEFAULT_OUTPUT_TAB = OUTPUT_TABS.DB_DESIGN diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/contexts/OutputUIContext.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/contexts/OutputUIContext.tsx new file mode 100644 index 0000000000..19c9ebd538 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/contexts/OutputUIContext.tsx @@ -0,0 +1,28 @@ +'use client' + +import type { Schema } from '@liam-hq/db-structure' +import { createContext } from 'react' +import type { ReviewComment } from '../components/DBDesign/components/SchemaUpdates/MigrationsViewer/useMigrationsViewer' + +type OutputUIState = { + selectedVersion: number +} + +export type VersionData = { + schema: Schema + artifactContent: string + schemaUpdatesDoc: string + comments: ReviewComment[] +} + +export type OutputUIContextType = { + state: OutputUIState + actions: { + setSelectedVersion: (version: number) => void + } + versionData: { + [version: number]: VersionData + } +} + +export const OutputUIContext = createContext(null) diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/hooks/useOutputUI.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/hooks/useOutputUI.tsx new file mode 100644 index 0000000000..c3fe3da31d --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/hooks/useOutputUI.tsx @@ -0,0 +1,17 @@ +'use client' + +import { useContext } from 'react' +import { + OutputUIContext, + type OutputUIContextType, +} from '../contexts/OutputUIContext' + +export const useOutputUI = (): OutputUIContextType => { + const context = useContext(OutputUIContext) + + if (!context) { + throw new Error('useOutputUI must be used within an OutputUIProvider') + } + + return context +} diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/index.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/index.ts new file mode 100644 index 0000000000..1c0b586d72 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/index.ts @@ -0,0 +1 @@ +export * from './Output' diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/mock/versionData.ts b/frontend/apps/app/components/SessionDetailPage/components/Output/mock/versionData.ts new file mode 100644 index 0000000000..686ac72e1d --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/mock/versionData.ts @@ -0,0 +1,761 @@ +import type { Schema } from '@liam-hq/db-structure' +import type { VersionData } from '../contexts/OutputUIContext' + +// v0 - シンプルなユーザー管理システム +const v0Schema: Schema = { + tables: { + users: { + name: 'users', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for users table', + }, + name: { + name: 'name', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'User full name', + }, + email: { + name: 'email', + type: 'text', + primary: false, + unique: true, + notNull: true, + default: null, + check: null, + comment: 'User email address', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + }, + comment: 'User accounts table', + indexes: {}, + constraints: {}, + }, + }, + relationships: {}, + tableGroups: {}, +} + +const v0ArtifactContent = `## データベース設計 v0 + +このバージョンでは、基本的なユーザー管理機能を提供します。 + +### 主要機能 +- ユーザーアカウント管理 +- 基本的なプロファイル情報 + +### テーブル構成 +- **users**: ユーザーの基本情報を管理 + +### 設計の特徴 +- シンプルな構造で必要最小限の機能を提供 +- 将来の拡張を考慮した設計` + +const v0SchemaUpdatesDoc = `-- v0: 基本的なユーザー管理システム +create table users ( + id bigint primary key generated always as identity, + name text not null, + email text not null unique, + created_at timestamp with time zone default now() +); + +comment on table users is 'User accounts table'; +comment on column users.id is 'Primary key for users table'; +comment on column users.name is 'User full name'; +comment on column users.email is 'User email address'; +comment on column users.created_at is 'Record creation timestamp';` + +const v0Comments = [ + { + fromLine: 3, + toLine: 3, + severity: 'Medium' as const, + message: + 'emailカラムにインデックスが自動作成されますが、ログイン機能を想定している場合は、追加でハッシュインデックスも検討してください。', + }, +] + +// v1 - プロジェクト管理機能を追加 +const v1Schema: Schema = { + tables: { + users: { + name: 'users', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for users table', + }, + name: { + name: 'name', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'User full name', + }, + email: { + name: 'email', + type: 'text', + primary: false, + unique: true, + notNull: true, + default: null, + check: null, + comment: 'User email address', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + updated_at: { + name: 'updated_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record update timestamp', + }, + }, + comment: 'User accounts table', + indexes: {}, + constraints: {}, + }, + projects: { + name: 'projects', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for projects table', + }, + name: { + name: 'name', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Project name', + }, + description: { + name: 'description', + type: 'text', + primary: false, + unique: false, + notNull: false, + default: null, + check: null, + comment: 'Project description', + }, + owner_id: { + name: 'owner_id', + type: 'bigint', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Project owner user ID', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + updated_at: { + name: 'updated_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record update timestamp', + }, + }, + comment: 'Projects table', + indexes: {}, + constraints: {}, + }, + }, + relationships: { + projects_owner: { + name: 'projects_owner', + primaryTableName: 'users', + primaryColumnName: 'id', + foreignTableName: 'projects', + foreignColumnName: 'owner_id', + cardinality: 'ONE_TO_MANY', + updateConstraint: 'CASCADE', + deleteConstraint: 'CASCADE', + }, + }, + tableGroups: {}, +} + +const v1ArtifactContent = `## データベース設計 v1 + +このバージョンでは、プロジェクト管理機能を追加しました。 + +### 新機能 +- プロジェクト作成・管理 +- プロジェクトオーナーシップ +- ユーザーとプロジェクトの関連付け + +### テーブル構成 +- **users**: ユーザーの基本情報(updated_atカラム追加) +- **projects**: プロジェクト情報を管理 + +### 設計の改善点 +- updated_atカラムの追加によりデータ更新の追跡が可能 +- 外部キー制約によるデータ整合性の確保 +- プロジェクトオーナーの明確な管理` + +const v1SchemaUpdatesDoc = `-- v1: プロジェクト管理機能を追加 +-- usersテーブルにupdated_atカラムを追加 +alter table users add column updated_at timestamp with time zone default now(); +comment on column users.updated_at is 'Record update timestamp'; + +-- projectsテーブルを作成 +create table projects ( + id bigint primary key generated always as identity, + name text not null, + description text, + owner_id bigint not null references users (id) on delete cascade, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +comment on table projects is 'Projects table'; +comment on column projects.id is 'Primary key for projects table'; +comment on column projects.name is 'Project name'; +comment on column projects.description is 'Project description'; +comment on column projects.owner_id is 'Project owner user ID'; +comment on column projects.created_at is 'Record creation timestamp'; +comment on column projects.updated_at is 'Record update timestamp'; + +-- インデックスを作成 +create index idx_projects_owner_id on projects (owner_id);` + +const v1Comments = [ + { + fromLine: 2, + toLine: 2, + severity: 'Medium' as const, + message: + 'updated_atカラムの自動更新にはトリガーが必要です。現在は手動更新となっています。', + }, + { + fromLine: 6, + toLine: 6, + severity: 'High' as const, + message: + 'CASCADE削除は危険な場合があります。ユーザー削除時に全プロジェクトが削除されるため、RESTRICT制約も検討してください。', + }, + { + fromLine: 19, + toLine: 19, + severity: 'Low' as const, + message: + 'プロジェクト検索用にname カラムにもインデックスを追加することを推奨します。', + }, +] + +// v2 - タスク管理機能を追加 +const v2Schema: Schema = { + tables: { + users: { + name: 'users', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for users table', + }, + name: { + name: 'name', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'User full name', + }, + email: { + name: 'email', + type: 'text', + primary: false, + unique: true, + notNull: true, + default: null, + check: null, + comment: 'User email address', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + updated_at: { + name: 'updated_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record update timestamp', + }, + }, + comment: 'User accounts table', + indexes: {}, + constraints: {}, + }, + projects: { + name: 'projects', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for projects table', + }, + name: { + name: 'name', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Project name', + }, + description: { + name: 'description', + type: 'text', + primary: false, + unique: false, + notNull: false, + default: null, + check: null, + comment: 'Project description', + }, + owner_id: { + name: 'owner_id', + type: 'bigint', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Project owner user ID', + }, + status: { + name: 'status', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: "'active'", + check: "status IN ('active', 'completed', 'archived')", + comment: 'Project status', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + updated_at: { + name: 'updated_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record update timestamp', + }, + }, + comment: 'Projects table', + indexes: {}, + constraints: {}, + }, + tasks: { + name: 'tasks', + columns: { + id: { + name: 'id', + type: 'bigint', + primary: true, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Primary key for tasks table', + }, + title: { + name: 'title', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Task title', + }, + description: { + name: 'description', + type: 'text', + primary: false, + unique: false, + notNull: false, + default: null, + check: null, + comment: 'Task description', + }, + project_id: { + name: 'project_id', + type: 'bigint', + primary: false, + unique: false, + notNull: true, + default: null, + check: null, + comment: 'Associated project ID', + }, + assignee_id: { + name: 'assignee_id', + type: 'bigint', + primary: false, + unique: false, + notNull: false, + default: null, + check: null, + comment: 'Assigned user ID', + }, + status: { + name: 'status', + type: 'text', + primary: false, + unique: false, + notNull: true, + default: "'pending'", + check: + "status IN ('pending', 'in_progress', 'completed', 'cancelled')", + comment: 'Task status', + }, + priority: { + name: 'priority', + type: 'integer', + primary: false, + unique: false, + notNull: true, + default: '3', + check: 'priority >= 1 AND priority <= 5', + comment: 'Task priority (1: highest, 5: lowest)', + }, + due_date: { + name: 'due_date', + type: 'date', + primary: false, + unique: false, + notNull: false, + default: null, + check: null, + comment: 'Task due date', + }, + created_at: { + name: 'created_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record creation timestamp', + }, + updated_at: { + name: 'updated_at', + type: 'timestamp with time zone', + primary: false, + unique: false, + notNull: true, + default: 'now()', + check: null, + comment: 'Record update timestamp', + }, + }, + comment: 'Tasks table', + indexes: {}, + constraints: {}, + }, + }, + relationships: { + projects_owner: { + name: 'projects_owner', + primaryTableName: 'users', + primaryColumnName: 'id', + foreignTableName: 'projects', + foreignColumnName: 'owner_id', + cardinality: 'ONE_TO_MANY', + updateConstraint: 'CASCADE', + deleteConstraint: 'RESTRICT', + }, + tasks_project: { + name: 'tasks_project', + primaryTableName: 'projects', + primaryColumnName: 'id', + foreignTableName: 'tasks', + foreignColumnName: 'project_id', + cardinality: 'ONE_TO_MANY', + updateConstraint: 'CASCADE', + deleteConstraint: 'CASCADE', + }, + tasks_assignee: { + name: 'tasks_assignee', + primaryTableName: 'users', + primaryColumnName: 'id', + foreignTableName: 'tasks', + foreignColumnName: 'assignee_id', + cardinality: 'ONE_TO_MANY', + updateConstraint: 'CASCADE', + deleteConstraint: 'SET_NULL', + }, + }, + tableGroups: { + user_management: { + name: 'user_management', + tables: ['users'], + comment: 'ユーザー管理関連テーブル', + }, + project_management: { + name: 'project_management', + tables: ['projects', 'tasks'], + comment: 'プロジェクト・タスク管理関連テーブル', + }, + }, +} + +const v2ArtifactContent = `## データベース設計 v2 + +このバージョンでは、タスク管理機能を追加し、プロジェクト管理システムを完成させました。 + +### 新機能 +- タスク作成・管理 +- タスクの担当者割り当て +- 優先度とステータス管理 +- 期限管理 +- テーブルグループによる論理的な分類 + +### テーブル構成 +- **users**: ユーザーの基本情報 +- **projects**: プロジェクト情報(ステータス管理追加) +- **tasks**: タスク情報を管理(新規追加) + +### 設計の改善点 +- プロジェクトにステータス管理を追加 +- CHECK制約による データ整合性の強化 +- 適切な外部キー制約(CASCADE/RESTRICT/SET_NULL) +- テーブルグループによる論理的な分類 + +### データ整合性 +- プロジェクトとタスクの関連性を保証 +- ユーザー削除時の適切な制約設定 +- ステータスと優先度の有効値制限` + +const v2SchemaUpdatesDoc = `-- usersテーブルにupdated_atカラムを追加 +alter table users add column updated_at timestamp with time zone default now(); +comment on column users.updated_at is 'Record update timestamp'; + +-- projectsテーブルを作成 +create table projects ( + id bigint primary key generated always as identity, + name text not null, + description text, + owner_id bigint not null references users (id) on delete cascade, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +comment on table projects is 'Projects table'; +comment on column projects.id is 'Primary key for projects table'; +comment on column projects.name is 'Project name'; +comment on column projects.description is 'Project description'; +comment on column projects.owner_id is 'Project owner user ID'; +comment on column projects.created_at is 'Record creation timestamp'; +comment on column projects.updated_at is 'Record update timestamp'; + +-- インデックスを作成 +create index idx_projects_owner_id on projects (owner_id); + +-- v2: タスク管理機能を追加 +-- projectsテーブルにstatusカラムを追加 +alter table projects add column status text not null default 'active'; +alter table projects add constraint projects_status_check + check (status in ('active', 'completed', 'archived')); +comment on column projects.status is 'Project status'; + +-- 外部キー制約を変更(より安全に) +alter table projects drop constraint projects_owner_id_fkey; +alter table projects add constraint projects_owner_id_fkey + foreign key (owner_id) references users (id) on delete restrict; + +-- tasksテーブルを作成 +create table tasks ( + id bigint primary key generated always as identity, + title text not null, + description text, + project_id bigint not null references projects (id) on delete cascade, + assignee_id bigint references users (id) on delete set null, + status text not null default 'pending', + priority integer not null default 3, + due_date date, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now(), + constraint tasks_status_check + check (status in ('pending', 'in_progress', 'completed', 'cancelled')), + constraint tasks_priority_check + check (priority >= 1 and priority <= 5) +); + +comment on table tasks is 'Tasks table'; +comment on column tasks.id is 'Primary key for tasks table'; +comment on column tasks.title is 'Task title'; +comment on column tasks.description is 'Task description'; +comment on column tasks.project_id is 'Associated project ID'; +comment on column tasks.assignee_id is 'Assigned user ID'; +comment on column tasks.status is 'Task status'; +comment on column tasks.priority is 'Task priority (1: highest, 5: lowest)'; +comment on column tasks.due_date is 'Task due date'; +comment on column tasks.created_at is 'Record creation timestamp'; +comment on column tasks.updated_at is 'Record update timestamp'; + +-- インデックスを作成 +create index idx_tasks_project_id on tasks (project_id); +create index idx_tasks_assignee_id on tasks (assignee_id); +create index idx_tasks_status on tasks (status); +create index idx_tasks_due_date on tasks (due_date); +create index idx_projects_status on projects (status);` + +const v2Comments = [ + { + fromLine: 8, + toLine: 9, + severity: 'High' as const, + message: + '外部キー制約の変更は既存データに影響を与える可能性があります。運用環境では慎重にマイグレーションを実行してください。', + }, + { + fromLine: 24, + toLine: 26, + severity: 'Medium' as const, + message: + 'CHECK制約により不正なデータの挿入は防げますが、アプリケーション側でも同様の検証を実装することを推奨します。', + }, + { + fromLine: 39, + toLine: 42, + severity: 'Low' as const, + message: + 'インデックスが適切に設定されています。タスクの検索性能が向上します。', + }, + { + fromLine: 15, + toLine: 15, + severity: 'Medium' as const, + message: + 'assignee_idがNULLの場合、未割り当てタスクとして扱われます。この仕様がビジネス要件と一致しているか確認してください。', + }, +] + +export const VERSION_DATA: Record = { + 0: { + schema: v0Schema, + artifactContent: v0ArtifactContent, + schemaUpdatesDoc: v0SchemaUpdatesDoc, + comments: v0Comments, + }, + 1: { + schema: v1Schema, + artifactContent: v1ArtifactContent, + schemaUpdatesDoc: v1SchemaUpdatesDoc, + comments: v1Comments, + }, + 2: { + schema: v2Schema, + artifactContent: v2ArtifactContent, + schemaUpdatesDoc: v2SchemaUpdatesDoc, + comments: v2Comments, + }, +} + +export const AVAILABLE_VERSIONS = [0, 1, 2] as const +export const DEFAULT_VERSION = 0 diff --git a/frontend/apps/app/components/SessionDetailPage/components/Output/providers/OutputUIProvider.tsx b/frontend/apps/app/components/SessionDetailPage/components/Output/providers/OutputUIProvider.tsx new file mode 100644 index 0000000000..7bab116b09 --- /dev/null +++ b/frontend/apps/app/components/SessionDetailPage/components/Output/providers/OutputUIProvider.tsx @@ -0,0 +1,30 @@ +'use client' + +import { type FC, type ReactNode, useState } from 'react' +import { OutputUIContext } from '../contexts/OutputUIContext' +import { DEFAULT_VERSION, VERSION_DATA } from '../mock/versionData' + +type Props = { + children: ReactNode +} + +export const OutputUIProvider: FC = ({ children }) => { + const [selectedVersion, setSelectedVersion] = + useState(DEFAULT_VERSION) + + const contextValue = { + state: { + selectedVersion, + }, + actions: { + setSelectedVersion, + }, + versionData: VERSION_DATA, + } + + return ( + + {children} + + ) +} diff --git a/frontend/apps/app/package.json b/frontend/apps/app/package.json index 0171427be1..aea1af81c9 100644 --- a/frontend/apps/app/package.json +++ b/frontend/apps/app/package.json @@ -8,6 +8,7 @@ "@codemirror/lang-yaml": "6.1.2", "@codemirror/language": "6.11.0", "@codemirror/lint": "6.8.5", + "@codemirror/merge": "6.10.2", "@codemirror/state": "6.5.2", "@codemirror/view": "6.36.8", "@electric-sql/pglite": "0.3.2", @@ -37,6 +38,7 @@ "react-dom": "19.1.0", "react-markdown": "10.1.0", "react-syntax-highlighter": "15.6.1", + "rehype-raw": "7.0.0", "remark-gfm": "4.0.1", "resend": "4.5.1", "ts-pattern": "5.7.1", diff --git a/frontend/packages/erd-core/package.json b/frontend/packages/erd-core/package.json index 7a09ddf8a3..88f376ad10 100644 --- a/frontend/packages/erd-core/package.json +++ b/frontend/packages/erd-core/package.json @@ -18,6 +18,7 @@ "lz-string": "1.5.0", "nuqs": "2.4.3", "react": "19.1.0", + "ts-pattern": "5.7.1", "valibot": "1.1.0" }, "devDependencies": { diff --git a/frontend/packages/erd-core/src/features/diff/utils/getChangeStatus.ts b/frontend/packages/erd-core/src/features/diff/utils/getChangeStatus.ts new file mode 100644 index 0000000000..b0a0c425f2 --- /dev/null +++ b/frontend/packages/erd-core/src/features/diff/utils/getChangeStatus.ts @@ -0,0 +1,18 @@ +import type { + SchemaDiffItem, + TableRelatedDiffItem, +} from '@liam-hq/db-structure' + +type Params = { + tableId: string + diffItems: SchemaDiffItem[] + kind: TableRelatedDiffItem['kind'] +} + +export function getChangeStatus({ tableId, diffItems, kind }: Params) { + const status = + diffItems.find((item) => item.kind === kind && item.tableId === tableId) + ?.status ?? 'unchanged' + + return status +} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.module.css b/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.module.css index dda8785120..b98158581b 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.module.css +++ b/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.module.css @@ -1,11 +1,66 @@ .wrapper { + display: grid; + grid-template-columns: auto 1fr; + width: 100%; + border-radius: var(--border-radius-md) var(--border-radius-md) 0px 0px; + background: var(--global-muted-background); +} + +.wrapperTableNameMode { + border-radius: var(--border-radius-md); +} + +.diffBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + aspect-ratio: 1; + border-top-left-radius: var(--border-radius-md); + background-color: var(--overlay-10); +} + +.diffBoxTableNameMode { + border-bottom-left-radius: var(--border-radius-md); +} + +.addedBg { + background-color: var(--color-green-alpha-20) !important; +} + +.removedBg { + background-color: var(--color-red-alpha-20) !important; +} + +.modifiedBg { + background-color: var(--color-yellow-alpha-20) !important; +} + +.diffIcon { + width: 0.75rem; + height: 0.75rem; +} + +.addedIcon { + color: var(--color-green-400); +} + +.removedIcon { + color: var(--color-red-500); +} + +.modifiedIcon { + color: #ffd748; +} + +.container { + flex: 1; display: flex; padding: var(--spacing-2); align-items: center; gap: var(--spacing-1); border-radius: var(--border-radius-md) var(--border-radius-md) 0px 0px; background: var(--global-muted-background); - backdrop-filter: blur(8px); color: var(--overlay-60); text-overflow: ellipsis; font-size: var(--font-size-5); @@ -13,13 +68,18 @@ line-height: normal; } -.wrapperTableNameMode { - border-bottom-left-radius: var(--border-radius-md); - border-bottom-right-radius: var(--border-radius-md); +.containerTableNameMode { + border-radius: var(--border-radius-md); +} + +.containerDiffView { + border-top-left-radius: var(--border-radius-none); + border-bottom-left-radius: var(--border-radius-none); } .tableIcon { - min-width: 16px; + min-width: 1rem; + width: 1rem; } .name { @@ -28,16 +88,6 @@ white-space: nowrap; } -@media (min-width: 767px) { - .name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; - } -} - .handle { top: auto; opacity: 0; diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.tsx index 13eec76130..b8f5d5767d 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDContent/components/TableNode/TableHeader/TableHeader.tsx @@ -1,10 +1,12 @@ +import { getChangeStatus } from '@/features/diff/utils/getChangeStatus' import type { TableNodeData } from '@/features/erd/types' import { useCustomReactflow } from '@/features/reactflow/hooks' -import { useUserEditing } from '@/stores' -import { Table2 } from '@liam-hq/ui' +import { useSchema, useUserEditing } from '@/stores' +import { Dot, Minus, Plus, Table2 } from '@liam-hq/ui' import { Handle, Position } from '@xyflow/react' import clsx from 'clsx' -import type { FC, MouseEvent } from 'react' +import { type FC, type MouseEvent, useMemo } from 'react' +import { match } from 'ts-pattern' import styles from './TableHeader.module.css' type Props = { @@ -13,12 +15,29 @@ type Props = { export const TableHeader: FC = ({ data }) => { const name = data.table.name - const { showMode: _showMode } = useUserEditing() + const { diffItems } = useSchema() + const { showMode: _showMode, showDiff } = useUserEditing() const showMode = data.showMode ?? _showMode const isTarget = data.targetColumnCardinalities !== undefined const isSource = data.sourceColumnName !== undefined + const tableStatus = getChangeStatus({ + tableId: name, + diffItems: diffItems ?? [], + kind: 'table', + }) + + const diffStyle = useMemo( + () => + match(tableStatus) + .with('added', () => styles.addedBg) + .with('removed', () => styles.removedBg) + .with('modified', () => styles.modifiedBg) + .otherwise(() => undefined), + [tableStatus], + ) + const { updateNode } = useCustomReactflow() const handleHoverEvent = (event: MouseEvent) => { @@ -48,32 +67,63 @@ export const TableHeader: FC = ({ data }) => { showMode === 'TABLE_NAME' && styles.wrapperTableNameMode, )} > - - - - {name} - - - {showMode === 'TABLE_NAME' && ( - <> - {isTarget && ( - + {showDiff && ( +
- )} - + > + {match(tableStatus) + .with('added', () => ( + + )) + .with('removed', () => ( + + )) + .with('modified', () => ( + + )) + .otherwise(() => null)} +
)} + +
+ + + + {name} + + + {showMode === 'TABLE_NAME' && ( + <> + {isTarget && ( + + )} + {isSource && ( + + )} + + )} +
) } diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalette.test.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalette.test.tsx index 4a4b7ef70e..6959f2b2a6 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalette.test.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalette.test.tsx @@ -1,5 +1,5 @@ import { SchemaProvider, UserEditingProvider } from '@/stores' -import type { SchemaStore } from '@/stores/schema/schema' +import type { SchemaProviderValue } from '@/stores/schema/schema' import { aTable } from '@liam-hq/db-structure' import { cleanup, render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -12,7 +12,7 @@ afterEach(() => { cleanup() }) -const schema: SchemaStore = { +const schema: SchemaProviderValue = { current: { tables: { users: aTable({ name: 'users' }), @@ -28,7 +28,7 @@ const schema: SchemaStore = { const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.tsx index 0594408422..c1eaf80ad3 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/ERDRenderer.tsx @@ -26,7 +26,7 @@ import { toggleLogEvent } from '@/features/gtm/utils' import { useIsTouchDevice } from '@/hooks' import { useVersion } from '@/providers' import { SchemaProvider, useSchema } from '@/stores' -import type { SchemaStore } from '@/stores/schema/schema' +import type { SchemaProviderValue } from '@/stores/schema/schema' import { UserEditingProvider, useUserEditing } from '@/stores/userEditing' import { convertSchemaToNodes, createHash } from '../../utils' import { ERDContent } from '../ERDContent' @@ -48,18 +48,19 @@ type InnerProps = { } type Props = InnerProps & { - schema: SchemaStore + schema: SchemaProviderValue + showDiff?: boolean } const SIDEBAR_COOKIE_NAME = 'sidebar:state' const PANEL_LAYOUT_COOKIE_NAME = 'panels:layout' const COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -export const ERDRenderer: FC = ({ schema, ...innerProps }) => { +export const ERDRenderer: FC = ({ schema, showDiff, ...innerProps }) => { return ( - - + + diff --git a/frontend/packages/erd-core/src/stores/schema/SchemaProvider.tsx b/frontend/packages/erd-core/src/stores/schema/SchemaProvider.tsx index 42b1fce367..98458b2294 100644 --- a/frontend/packages/erd-core/src/stores/schema/SchemaProvider.tsx +++ b/frontend/packages/erd-core/src/stores/schema/SchemaProvider.tsx @@ -1,13 +1,26 @@ +import { buildSchemaDiff } from '@liam-hq/db-structure' import type { FC, PropsWithChildren } from 'react' +import { useMemo } from 'react' import { SchemaContext } from './context' -import type { SchemaStore } from './schema' +import type { SchemaContextValue, SchemaProviderValue } from './schema' -type Props = PropsWithChildren & { - schema: SchemaStore -} +type Props = PropsWithChildren & SchemaProviderValue + +export const SchemaProvider: FC = ({ children, current, previous }) => { + const computedSchema: SchemaContextValue = useMemo(() => { + const diffItems = + current && previous ? buildSchemaDiff(previous, current) : undefined + + return { + current, + previous, + diffItems, + } + }, [current, previous]) -export const SchemaProvider: FC = ({ children, schema }) => { return ( - {children} + + {children} + ) } diff --git a/frontend/packages/erd-core/src/stores/schema/context.ts b/frontend/packages/erd-core/src/stores/schema/context.ts index 2e188cce66..9f8dd89f70 100644 --- a/frontend/packages/erd-core/src/stores/schema/context.ts +++ b/frontend/packages/erd-core/src/stores/schema/context.ts @@ -1,4 +1,4 @@ import { createContext } from 'react' -import type { SchemaStore } from './schema' +import type { SchemaContextValue } from './schema' -export const SchemaContext = createContext(null) +export const SchemaContext = createContext(null) diff --git a/frontend/packages/erd-core/src/stores/schema/schema.ts b/frontend/packages/erd-core/src/stores/schema/schema.ts index 79564596f2..aec0782585 100644 --- a/frontend/packages/erd-core/src/stores/schema/schema.ts +++ b/frontend/packages/erd-core/src/stores/schema/schema.ts @@ -1,10 +1,18 @@ import { schemaDiffItemsSchema, schemaSchema } from '@liam-hq/db-structure' import * as v from 'valibot' -const schemaStoreSchema = v.object({ +// Type for Schema Provider (received from external sources, does not include diffItems) +const schemaProviderSchema = v.object({ + current: schemaSchema, + previous: v.optional(schemaSchema), +}) + +// Type for Schema Context (used internally, diffItems are automatically calculated) +const schemaContextSchema = v.object({ current: schemaSchema, previous: v.optional(schemaSchema), diffItems: v.optional(schemaDiffItemsSchema), }) -export type SchemaStore = v.InferOutput +export type SchemaProviderValue = v.InferOutput +export type SchemaContextValue = v.InferOutput diff --git a/frontend/packages/erd-core/src/stores/userEditing/Provider.tsx b/frontend/packages/erd-core/src/stores/userEditing/UserEditingProvider.tsx similarity index 90% rename from frontend/packages/erd-core/src/stores/userEditing/Provider.tsx rename to frontend/packages/erd-core/src/stores/userEditing/UserEditingProvider.tsx index f61540c5f0..c46dc10d54 100644 --- a/frontend/packages/erd-core/src/stores/userEditing/Provider.tsx +++ b/frontend/packages/erd-core/src/stores/userEditing/UserEditingProvider.tsx @@ -12,8 +12,15 @@ import { parseAsStringEnum, useQueryState, } from 'nuqs' -import { type FC, type PropsWithChildren, useCallback, useState } from 'react' +import { + type FC, + type PropsWithChildren, + useCallback, + useEffect, + useState, +} from 'react' import { UserEditingContext } from './context' +import type { UserEditingProviderValue } from './types' const parseAsCompressedStringArray = createParser({ parse: (value: string): string[] => { @@ -33,7 +40,12 @@ const parseAsCompressedStringArray = createParser({ }, }) -export const UserEditingProvider: FC = ({ children }) => { +type UserEditingProviderProps = PropsWithChildren & UserEditingProviderValue + +export const UserEditingProvider: FC = ({ + children, + showDiff: initialShowDiff = false, +}) => { const [activeTableName, setActiveTableName] = useQueryState( 'active', parseAsString.withDefault('').withOptions({ history: 'push' }), @@ -58,6 +70,11 @@ export const UserEditingProvider: FC = ({ children }) => { const [selectedNodeIds, setSelectedNodeIds] = useState>(new Set()) const [isPopstateInProgress, setIsPopstateInProgress] = useState(false) const [isTableGroupEditMode, setIsTableGroupEditMode] = useState(false) + const [showDiff, setShowDiff] = useState(initialShowDiff) + + useEffect(() => { + setShowDiff(initialShowDiff) + }, [initialShowDiff]) const toggleHiddenNodeId = useCallback( (nodeId: string) => { @@ -196,6 +213,8 @@ export const UserEditingProvider: FC = ({ children }) => { setIsPopstateInProgress, isTableGroupEditMode, setIsTableGroupEditMode, + showDiff, + setShowDiff, }} > {children} diff --git a/frontend/packages/erd-core/src/stores/userEditing/context.ts b/frontend/packages/erd-core/src/stores/userEditing/context.ts index d847bc33f4..9940a0dd14 100644 --- a/frontend/packages/erd-core/src/stores/userEditing/context.ts +++ b/frontend/packages/erd-core/src/stores/userEditing/context.ts @@ -26,6 +26,8 @@ type UserEditingContextValue = { setIsPopstateInProgress: (isPopstateInProgress: boolean) => void isTableGroupEditMode: boolean setIsTableGroupEditMode: (isGroupEditMode: boolean) => void + showDiff: boolean + setShowDiff: (showDiff: boolean) => void } export const UserEditingContext = createContext( diff --git a/frontend/packages/erd-core/src/stores/userEditing/index.ts b/frontend/packages/erd-core/src/stores/userEditing/index.ts index 61c308016e..cfa67315e5 100644 --- a/frontend/packages/erd-core/src/stores/userEditing/index.ts +++ b/frontend/packages/erd-core/src/stores/userEditing/index.ts @@ -1,2 +1,2 @@ export * from './hooks' -export * from './Provider' +export * from './UserEditingProvider' diff --git a/frontend/packages/erd-core/src/stores/userEditing/types.ts b/frontend/packages/erd-core/src/stores/userEditing/types.ts new file mode 100644 index 0000000000..11c6fd60fe --- /dev/null +++ b/frontend/packages/erd-core/src/stores/userEditing/types.ts @@ -0,0 +1,3 @@ +export type UserEditingProviderValue = { + showDiff?: boolean | undefined +} diff --git a/frontend/packages/ui/src/icons/index.ts b/frontend/packages/ui/src/icons/index.ts index 2ee5001e78..722da07fba 100644 --- a/frontend/packages/ui/src/icons/index.ts +++ b/frontend/packages/ui/src/icons/index.ts @@ -23,6 +23,7 @@ export { ClipboardList, Copy, CornerDownLeft, + Dot, Download, Ellipsis, Eye, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0f889d64f..7c85f78862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ importers: '@codemirror/lint': specifier: 6.8.5 version: 6.8.5 + '@codemirror/merge': + specifier: 6.10.2 + version: 6.10.2 '@codemirror/state': specifier: 6.5.2 version: 6.5.2 @@ -108,13 +111,13 @@ importers: version: 15.3.3(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) '@sentry/nextjs': specifier: '9' - version: 9.27.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9(esbuild@0.25.4)) + version: 9.27.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9) '@types/react-syntax-highlighter': specifier: 15.5.13 version: 15.5.13 '@vercel/otel': specifier: 1.12.0 - version: 1.12.0(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) + version: 1.12.0(@opentelemetry/api@1.9.0)(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) cheerio: specifier: 1.0.0 version: 1.0.0 @@ -154,6 +157,9 @@ importers: react-syntax-highlighter: specifier: 15.6.1 version: 15.6.1(react@19.1.0) + rehype-raw: + specifier: 7.0.0 + version: 7.0.0 remark-gfm: specifier: 4.0.1 version: 4.0.1 @@ -317,7 +323,7 @@ importers: version: 3.1.1 langfuse-langchain: specifier: 3.37.3 - version: 3.37.3(langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0(debug@4.4.1))(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2)) + version: 3.37.3(langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2)) valibot: specifier: 1.1.0 version: 1.1.0(typescript@5.8.3) @@ -576,7 +582,7 @@ importers: version: 8.6.14(react@19.1.0)(storybook@8.6.14(prettier@3.5.3)) '@storybook/nextjs': specifier: 8.6.14 - version: 8.6.14(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9) + version: 8.6.14(esbuild@0.25.4)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4)) '@storybook/react': specifier: 8.6.14 version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) @@ -765,6 +771,9 @@ importers: react: specifier: 19.1.0 version: 19.1.0 + ts-pattern: + specifier: 5.7.1 + version: 5.7.1 valibot: specifier: 1.1.0 version: 1.1.0(typescript@5.8.3) @@ -1689,6 +1698,9 @@ packages: '@codemirror/lint@6.8.5': resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} + '@codemirror/merge@6.10.2': + resolution: {integrity: sha512-rmHzVkt5FnCtsi0IgvDIDjh/J4LmbfOboB7FMvVl21IHO0p1QM6jSwjkBjBD3D+c+T79OabEqoduCqvJCBV8Yg==} + '@codemirror/search@6.5.11': resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} @@ -7343,9 +7355,18 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -7355,6 +7376,9 @@ packages: hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} @@ -7364,6 +7388,9 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -9342,6 +9369,9 @@ packages: property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -9604,6 +9634,9 @@ packages: resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} @@ -10792,6 +10825,9 @@ packages: engines: {node: '>= 18'} hasBin: true + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -10900,6 +10936,9 @@ packages: resolution: {integrity: sha512-C/sVYLqLuRQn2FeP3YQFNSOV61gNNtswNDRnb117ZfweSU/DIGAYeZZn63DpNI11qDSWTS3Cbz/aQjUzZXbA6Q==} engines: {node: '>=18.0.0'} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -12288,6 +12327,14 @@ snapshots: '@codemirror/view': 6.36.8 crelt: 1.0.6 + '@codemirror/merge@6.10.2': + dependencies: + '@codemirror/language': 6.11.0 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.36.8 + '@lezer/highlight': 1.2.1 + style-mod: 4.1.2 + '@codemirror/search@6.5.11': dependencies: '@codemirror/state': 6.5.2 @@ -12923,7 +12970,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.4.0 js-yaml: 4.1.0 - langchain: 0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0(debug@4.4.1))(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2) + langchain: 0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2) langsmith: 0.3.30(openai@4.104.0(ws@8.18.2)(zod@3.24.4)) openai: 4.104.0(ws@8.18.2)(zod@3.24.4) uuid: 10.0.0 @@ -13145,7 +13192,7 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 express: 5.1.0(supports-color@10.0.0) - express-rate-limit: 7.5.0(express@5.1.0) + express-rate-limit: 7.5.0(express@5.1.0(supports-color@10.0.0)) pkce-challenge: 5.0.0 raw-body: 3.0.0 zod: 3.24.4 @@ -13821,7 +13868,7 @@ snapshots: dependencies: playwright: 1.52.0 - '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9)': + '@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4))': dependencies: ansi-html: 0.0.9 core-js-pure: 3.42.0 @@ -13831,7 +13878,7 @@ snapshots: react-refresh: 0.14.2 schema-utils: 4.3.2 source-map: 0.7.4 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) optionalDependencies: type-fest: 4.41.0 webpack-hot-middleware: 2.26.1 @@ -14774,33 +14821,6 @@ snapshots: - encoding - supports-color - '@sentry/nextjs@9.27.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9(esbuild@0.25.4))': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.34.0 - '@rollup/plugin-commonjs': 28.0.1(rollup@4.35.0) - '@sentry-internal/browser-utils': 9.27.0 - '@sentry/core': 9.27.0 - '@sentry/node': 9.27.0 - '@sentry/opentelemetry': 9.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) - '@sentry/react': 9.27.0(react@19.1.0) - '@sentry/vercel-edge': 9.27.0 - '@sentry/webpack-plugin': 3.5.0(webpack@5.99.9(esbuild@0.25.4)) - chalk: 3.0.0 - next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - resolve: 1.22.8 - rollup: 4.35.0 - stacktrace-parser: 0.1.11 - transitivePeerDependencies: - - '@opentelemetry/context-async-hooks' - - '@opentelemetry/core' - - '@opentelemetry/instrumentation' - - '@opentelemetry/sdk-trace-base' - - encoding - - react - - supports-color - - webpack - '@sentry/nextjs@9.27.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.9)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14938,16 +14958,6 @@ snapshots: '@opentelemetry/api': 1.9.0 '@sentry/core': 9.27.0 - '@sentry/webpack-plugin@3.5.0(webpack@5.99.9(esbuild@0.25.4))': - dependencies: - '@sentry/bundler-plugin-core': 3.5.0 - unplugin: 1.0.1 - uuid: 9.0.1 - webpack: 5.99.9(esbuild@0.25.4) - transitivePeerDependencies: - - encoding - - supports-color - '@sentry/webpack-plugin@3.5.0(webpack@5.99.9)': dependencies: '@sentry/bundler-plugin-core': 3.5.0 @@ -15117,7 +15127,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-webpack5@8.6.14(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': + '@storybook/builder-webpack5@8.6.14(esbuild@0.25.4)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@types/semver': 7.7.0 @@ -15125,23 +15135,23 @@ snapshots: case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 constants-browserify: 1.0.0 - css-loader: 6.11.0(webpack@5.99.9) + css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.4)) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9) - html-webpack-plugin: 5.6.3(webpack@5.99.9) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) + html-webpack-plugin: 5.6.3(webpack@5.99.9(esbuild@0.25.4)) magic-string: 0.30.17 path-browserify: 1.0.1 process: 0.11.10 semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) - style-loader: 3.3.4(webpack@5.99.9) - terser-webpack-plugin: 5.3.14(webpack@5.99.9) + style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.4)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.4)(webpack@5.99.9(esbuild@0.25.4)) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.99.9 - webpack-dev-middleware: 6.1.3(webpack@5.99.9) + webpack: 5.99.9(esbuild@0.25.4) + webpack-dev-middleware: 6.1.3(webpack@5.99.9(esbuild@0.25.4)) webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: @@ -15205,7 +15215,7 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) - '@storybook/nextjs@8.6.14(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9)': + '@storybook/nextjs@8.6.14(esbuild@0.25.4)(next@15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(type-fest@4.41.0)(typescript@5.8.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.4) @@ -15220,30 +15230,30 @@ snapshots: '@babel/preset-react': 7.27.1(@babel/core@7.27.4) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.4) '@babel/runtime': 7.27.6 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9) - '@storybook/builder-webpack5': 8.6.14(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) - '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.99.9(esbuild@0.25.4)) + '@storybook/builder-webpack5': 8.6.14(esbuild@0.25.4)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) + '@storybook/preset-react-webpack': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(esbuild@0.25.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) '@storybook/test': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@types/semver': 7.7.0 - babel-loader: 9.2.1(@babel/core@7.27.4)(webpack@5.99.9) - css-loader: 6.11.0(webpack@5.99.9) + babel-loader: 9.2.1(@babel/core@7.27.4)(webpack@5.99.9(esbuild@0.25.4)) + css-loader: 6.11.0(webpack@5.99.9(esbuild@0.25.4)) find-up: 5.0.0 image-size: 1.2.1 loader-utils: 3.3.1 next: 15.3.3(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9) + node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.9(esbuild@0.25.4)) pnp-webpack-plugin: 1.7.0(typescript@5.8.3) postcss: 8.5.3 - postcss-loader: 8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9) + postcss-loader: 8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) react-refresh: 0.14.2 resolve-url-loader: 5.0.0 - sass-loader: 14.2.1(webpack@5.99.9) + sass-loader: 14.2.1(webpack@5.99.9(esbuild@0.25.4)) semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) - style-loader: 3.3.4(webpack@5.99.9) + style-loader: 3.3.4(webpack@5.99.9(esbuild@0.25.4)) styled-jsx: 5.1.7(@babel/core@7.27.4)(react@19.1.0) ts-dedent: 2.2.0 tsconfig-paths: 4.2.0 @@ -15251,7 +15261,7 @@ snapshots: optionalDependencies: sharp: 0.33.5 typescript: 5.8.3 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -15270,11 +15280,11 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': + '@storybook/preset-react-webpack@8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(esbuild@0.25.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3)': dependencies: '@storybook/core-webpack': 8.6.14(storybook@8.6.14(prettier@3.5.3)) '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.14(prettier@3.5.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.14(prettier@3.5.3))(typescript@5.8.3) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)) '@types/semver': 7.7.0 find-up: 5.0.0 magic-string: 0.30.17 @@ -15285,7 +15295,7 @@ snapshots: semver: 7.7.2 storybook: 8.6.14(prettier@3.5.3) tsconfig-paths: 4.2.0 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -15300,7 +15310,7 @@ snapshots: dependencies: storybook: 8.6.14(prettier@3.5.3) - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9)': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4))': dependencies: debug: 4.4.1(supports-color@10.0.0) endent: 2.1.0 @@ -15310,7 +15320,7 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@5.8.3) tslib: 2.8.1 typescript: 5.8.3 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) transitivePeerDependencies: - supports-color @@ -16040,10 +16050,9 @@ snapshots: - rollup - supports-color - '@vercel/otel@1.12.0(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))': + '@vercel/otel@1.12.0(@opentelemetry/api@1.9.0)(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) @@ -16508,12 +16517,12 @@ snapshots: transitivePeerDependencies: - debug - babel-loader@9.2.1(@babel/core@7.27.4)(webpack@5.99.9): + babel-loader@9.2.1(@babel/core@7.27.4)(webpack@5.99.9(esbuild@0.25.4)): dependencies: '@babel/core': 7.27.4 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.27.4): dependencies: @@ -17163,7 +17172,7 @@ snapshots: randombytes: 2.1.0 randomfill: 1.0.4 - css-loader@6.11.0(webpack@5.99.9): + css-loader@6.11.0(webpack@5.99.9(esbuild@0.25.4)): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -17174,7 +17183,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) css-select@4.3.0: dependencies: @@ -17816,7 +17825,7 @@ snapshots: expr-eval@2.0.2: {} - express-rate-limit@7.5.0(express@5.1.0): + express-rate-limit@7.5.0(express@5.1.0(supports-color@10.0.0)): dependencies: express: 5.1.0(supports-color@10.0.0) @@ -18024,7 +18033,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -18039,7 +18048,7 @@ snapshots: semver: 7.7.2 tapable: 2.2.2 typescript: 5.8.3 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) form-data-encoder@1.7.2: {} @@ -18439,8 +18448,39 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + hast-util-parse-selector@2.2.5: {} + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.8 @@ -18496,6 +18536,16 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-string@3.0.1: dependencies: '@types/hast': 3.0.4 @@ -18512,6 +18562,14 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + he@1.2.0: {} header-case@1.0.1: @@ -18561,7 +18619,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(webpack@5.99.9): + html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -18569,7 +18627,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) htmlparser2@6.1.0: dependencies: @@ -18665,7 +18723,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.9.0(debug@4.4.1)) + retry-axios: 2.6.0(axios@1.9.0) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -19113,7 +19171,7 @@ snapshots: zod: 3.24.4 zod-validation-error: 3.4.1(zod@3.24.4) - langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0(debug@4.4.1))(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2): + langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2): dependencies: '@langchain/core': 0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)) '@langchain/openai': 0.5.11(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(ws@8.18.2) @@ -19141,9 +19199,9 @@ snapshots: dependencies: mustache: 4.2.0 - langfuse-langchain@3.37.3(langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0(debug@4.4.1))(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2)): + langfuse-langchain@3.37.3(langchain@0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2)): dependencies: - langchain: 0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0(debug@4.4.1))(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2) + langchain: 0.3.27(@langchain/core@0.3.57(openai@4.104.0(ws@8.18.2)(zod@3.24.4)))(axios@1.9.0)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(ws@8.18.2)(zod@3.24.4))(ws@8.18.2) langfuse: 3.37.4 langfuse-core: 3.37.4 @@ -20060,7 +20118,7 @@ snapshots: mkdirp: 0.5.6 resolve: 1.22.10 - node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9): + node-polyfill-webpack-plugin@2.0.1(webpack@5.99.9(esbuild@0.25.4)): dependencies: assert: 2.1.0 browserify-zlib: 0.2.0 @@ -20087,7 +20145,7 @@ snapshots: url: 0.11.4 util: 0.12.5 vm-browserify: 1.1.2 - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) node-releases@2.0.19: {} @@ -20662,14 +20720,14 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-loader@8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9): + postcss-loader@8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.4)): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 postcss: 8.5.3 semver: 7.7.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) transitivePeerDependencies: - typescript @@ -20797,6 +20855,8 @@ snapshots: dependencies: xtend: 4.0.2 + property-information@6.5.0: {} + property-information@7.1.0: {} protobufjs@7.5.3: @@ -21148,6 +21208,12 @@ snapshots: dependencies: jsesc: 3.0.2 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -21273,7 +21339,7 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retry-axios@2.6.0(axios@1.9.0(debug@4.4.1)): + retry-axios@2.6.0(axios@1.9.0): dependencies: axios: 1.9.0(debug@4.4.1) @@ -21391,11 +21457,11 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@14.2.1(webpack@5.99.9): + sass-loader@14.2.1(webpack@5.99.9(esbuild@0.25.4)): dependencies: neo-async: 2.6.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) scheduler@0.26.0: {} @@ -21849,9 +21915,9 @@ snapshots: prettier: 3.5.3 tinycolor2: 1.6.0 - style-loader@3.3.4(webpack@5.99.9): + style-loader@3.3.4(webpack@5.99.9(esbuild@0.25.4)): dependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) style-mod@4.1.2: {} @@ -22547,6 +22613,11 @@ snapshots: - rollup - supports-color + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -22676,6 +22747,8 @@ snapshots: transitivePeerDependencies: - encoding + web-namespaces@2.0.1: {} + web-streams-polyfill@3.3.3: {} web-streams-polyfill@4.0.0-beta.3: {} @@ -22686,7 +22759,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@6.1.3(webpack@5.99.9): + webpack-dev-middleware@6.1.3(webpack@5.99.9(esbuild@0.25.4)): dependencies: colorette: 2.0.20 memfs: 3.5.3 @@ -22694,7 +22767,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.2 optionalDependencies: - webpack: 5.99.9 + webpack: 5.99.9(esbuild@0.25.4) webpack-hot-middleware@2.26.1: dependencies: