Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const PublicSessionDetailPage = async ({
<SessionDetailPageClient
buildingSchemaId={buildingSchemaId}
designSessionId={designSessionId}
sessionTitle={designSession.name}
initialMessages={[]}
initialAnalyzedRequirements={initialAnalyzedRequirements}
initialDisplayedSchema={initialSchema}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async function loadSessionData(designSessionId: string): Promise<
baselineSchema: Schema
initialAnalyzedRequirements: AnalyzedRequirements | null
workflowError: string | null
sessionTitle: string | null
},
Error
>
Expand All @@ -49,6 +50,13 @@ async function loadSessionData(designSessionId: string): Promise<
}

const supabase = await createClient()
const { data: sessionData } = await supabase
.from('design_sessions')
.select('name')
.eq('id', designSessionId)
.single()
const sessionTitle = sessionData?.name ?? null

const organizationId = buildingSchema.organization_id
const repositories = createSupabaseRepositories(supabase, organizationId)
const config = {
Expand Down Expand Up @@ -91,6 +99,7 @@ async function loadSessionData(designSessionId: string): Promise<
baselineSchema,
initialAnalyzedRequirements,
workflowError,
sessionTitle,
})
}

Expand All @@ -108,6 +117,7 @@ export const SessionDetailPage: FC<Props> = async ({ designSessionId }) => {
baselineSchema,
workflowError,
initialAnalyzedRequirements,
sessionTitle,
} = result.value

const versions = await getVersions(buildingSchema.id)
Expand All @@ -132,6 +142,7 @@ export const SessionDetailPage: FC<Props> = async ({ designSessionId }) => {
<SessionDetailPageClient
buildingSchemaId={buildingSchema.id}
designSessionId={designSessionId}
sessionTitle={sessionTitle}
initialMessages={messages}
initialAnalyzedRequirements={initialAnalyzedRequirements}
initialDisplayedSchema={initialSchema}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const COOKIE_MAX_AGE = 60 * 60 * 24 * 7
type Props = {
buildingSchemaId: string
designSessionId: string
sessionTitle: string | null
initialMessages: StoredMessage[]
initialAnalyzedRequirements: AnalyzedRequirements | null
initialDisplayedSchema: Schema
Expand All @@ -56,6 +57,7 @@ const determineInitialTab = (
export const SessionDetailPageClient: FC<Props> = ({
buildingSchemaId,
designSessionId,
sessionTitle,
initialMessages,
initialAnalyzedRequirements,
initialDisplayedSchema,
Expand Down Expand Up @@ -176,6 +178,7 @@ export const SessionDetailPageClient: FC<Props> = ({
<div className={styles.chatSection}>
<div className={styles.chatWrapper}>
<Chat
sessionTitle={sessionTitle ?? undefined}
messages={messages}
isWorkflowRunning={isStreaming}
onNavigate={setActiveTab}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import type { FC } from 'react'
import { useCallback, useEffect, useState } from 'react'
import type { OutputTabValue } from '../Output/constants'
import styles from './Chat.module.css'
import { ChatHeader } from './components/ChatHeader'
import { ErrorDisplay } from './components/ErrorDisplay'
import { Messages } from './components/Messages'
import { ScrollToBottomButton } from './components/ScrollToBottomButton'
import { WorkflowRunningIndicator } from './components/WorkflowRunningIndicator'
import { useScrollToBottom } from './useScrollToBottom'

type Props = {
sessionTitle?: string
messages: BaseMessage[]
isWorkflowRunning?: boolean
error?: string | null
onNavigate: (tab: OutputTabValue) => void
}

export const Chat: FC<Props> = ({
sessionTitle,
messages,
isWorkflowRunning = false,
onNavigate,
Expand Down Expand Up @@ -52,6 +55,11 @@ export const Chat: FC<Props> = ({

return (
<div className={styles.wrapper}>
<ChatHeader
sessionTitle={sessionTitle}
messages={messages}
isWorkflowRunning={isWorkflowRunning}
/>
<div className={styles.messageListWrapper}>
<div className={styles.messageList} ref={containerRef}>
<Messages
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.header {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--pane-base-background);
border-bottom: 1px solid var(--pane-base-border);
padding: var(--spacing-3) var(--spacing-4);
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}

.titleSection {
display: flex;
align-items: center;
}

.sessionTitle {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.statusSection {
display: flex;
align-items: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client'

import type { BaseMessage } from '@langchain/core/messages'
import { isAIMessage } from '@langchain/core/messages'
import type { FC } from 'react'
import { useMemo } from 'react'
import * as v from 'valibot'
import styles from './ChatHeader.module.css'
import { AgentStatusIndicator } from './components/AgentStatusIndicator'

type Props = {
sessionTitle?: string
messages: BaseMessage[]
isWorkflowRunning?: boolean
}

const agentRoleSchema = v.picklist(['db', 'pm', 'qa', 'lead'])

type AgentRole = 'pm' | 'db' | 'qa'

const isValidAgentRole = (role: string): role is AgentRole => {
return role === 'pm' || role === 'db' || role === 'qa'
}

const findLastNonLeadAgent = (aiMessages: BaseMessage[]): AgentRole | null => {
for (let i = aiMessages.length - 1; i >= 0; i--) {
const msg = aiMessages[i]
if (!msg) continue

const role = v.safeParse(agentRoleSchema, msg.name)
if (
role.success &&
role.output !== 'lead' &&
isValidAgentRole(role.output)
) {
return role.output
}
}
return 'pm'
}

const extractCurrentAgent = (
messages: BaseMessage[],
isWorkflowRunning: boolean,
): AgentRole | null => {
if (!isWorkflowRunning) {
return null
}

const aiMessages = messages.filter((msg) => isAIMessage(msg))
if (aiMessages.length === 0) {
return 'pm'
}

const lastAiMessage = aiMessages[aiMessages.length - 1]
if (!lastAiMessage) {
return null
}

const parsed = v.safeParse(agentRoleSchema, lastAiMessage.name)
if (!parsed.success) {
return null
}

if (parsed.output === 'lead') {
return findLastNonLeadAgent(aiMessages)
}

return isValidAgentRole(parsed.output) ? parsed.output : null
}

const extractCompletedAgents = (messages: BaseMessage[]): AgentRole[] => {
const completed: Set<AgentRole> = new Set()
const aiMessages = messages.filter((msg) => isAIMessage(msg))

for (const msg of aiMessages) {
const parsed = v.safeParse(agentRoleSchema, msg.name)
if (
parsed.success &&
parsed.output !== 'lead' &&
isValidAgentRole(parsed.output)
) {
completed.add(parsed.output)
}
}

return Array.from(completed)
}

export const ChatHeader: FC<Props> = ({
sessionTitle,
messages,
isWorkflowRunning = false,
}) => {
const currentAgent = useMemo(
() => extractCurrentAgent(messages, isWorkflowRunning),
[messages, isWorkflowRunning],
)

const completedAgents = useMemo(
() => extractCompletedAgents(messages),
[messages],
)

return (
<div className={styles.header}>
<div className={styles.titleSection}>
{sessionTitle && (
<h2 className={styles.sessionTitle}>{sessionTitle}</h2>
)}
</div>
{isWorkflowRunning && (
<div className={styles.statusSection}>
<AgentStatusIndicator
currentAgent={currentAgent}
completedAgents={completedAgents}
/>
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.container {
display: flex;
align-items: center;
gap: var(--spacing-2);
font-size: var(--font-size-sm);
}

.label {
color: var(--text-secondary);
font-weight: var(--font-weight-medium);
}

.agentList {
display: flex;
align-items: center;
gap: var(--spacing-1);
flex-wrap: wrap;
}

.agentItem {
display: flex;
align-items: center;
}

.agentName {
display: inline-flex;
align-items: center;
gap: var(--spacing-1);
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--border-radius-sm);
transition: all 0.2s ease;
}

.agentName[data-status='completed'] {
color: var(--text-primary);
background-color: var(--overlay-10);
}

.agentName[data-status='running'] {
color: var(--text-primary);
background-color: var(--overlay-20);
font-weight: var(--font-weight-semibold);
}

.agentName[data-status='pending'] {
color: var(--text-tertiary);
}

.checkmark {
color: var(--text-success);
font-weight: var(--font-weight-bold);
}

.runningIndicator {
display: inline-flex;
align-items: center;
margin-left: var(--spacing-1);
}

.arrow {
color: var(--text-tertiary);
margin: 0 var(--spacing-1);
}
Loading
Loading