feat: share sessions in the UI (#1727)
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Clock, MessageSquare, Folder, AlertCircle } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Clock, MessageSquare, Folder, Share, Copy, Check, LoaderCircle } from 'lucide-react';
|
||||
import { type SessionDetails } from '../../sessions';
|
||||
import { Card } from '../ui/card';
|
||||
import { SessionHeaderCard, SessionMessages } from './SessionViewComponents';
|
||||
import { createSharedSession } from '../../sharedSessions';
|
||||
import { Modal, ModalContent, ModalHeader, ModalTitle, ModalFooter } from '../ui/modal';
|
||||
import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import MarkdownContent from '../MarkdownContent';
|
||||
import ToolCallWithResponse from '../ToolCallWithResponse';
|
||||
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface SessionHistoryViewProps {
|
||||
session: SessionDetails;
|
||||
@@ -18,31 +16,6 @@ interface SessionHistoryViewProps {
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
export const getToolResponsesMap = (
|
||||
session: SessionDetails,
|
||||
messageIndex: number,
|
||||
toolRequests: ToolRequestMessageContent[]
|
||||
) => {
|
||||
const responseMap = new Map();
|
||||
|
||||
if (messageIndex >= 0) {
|
||||
for (let i = messageIndex + 1; i < session.messages.length; i++) {
|
||||
const responses = session.messages[i].content
|
||||
.filter((c) => c.type === 'toolResponse')
|
||||
.map((c) => c as ToolResponseMessageContent);
|
||||
|
||||
for (const response of responses) {
|
||||
const matchingRequest = toolRequests.find((req) => req.id === response.id);
|
||||
if (matchingRequest) {
|
||||
responseMap.set(response.id, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responseMap;
|
||||
};
|
||||
|
||||
const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
session,
|
||||
isLoading,
|
||||
@@ -51,14 +24,85 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
onResume,
|
||||
onRetry,
|
||||
}) => {
|
||||
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
||||
const [shareLink, setShareLink] = useState<string>('');
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [canShare, setCanShare] = useState(false);
|
||||
const [shareError, setShareError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const savedSessionConfig = localStorage.getItem('session_sharing_config');
|
||||
if (savedSessionConfig) {
|
||||
try {
|
||||
const config = JSON.parse(savedSessionConfig);
|
||||
// If config.enabled is true and config.baseUrl is non-empty, we can share
|
||||
if (config.enabled && config.baseUrl) {
|
||||
setCanShare(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing session sharing config:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleShare = async () => {
|
||||
setIsSharing(true);
|
||||
setShareError(null);
|
||||
|
||||
try {
|
||||
// Get the session sharing configuration from localStorage
|
||||
const savedSessionConfig = localStorage.getItem('session_sharing_config');
|
||||
if (!savedSessionConfig) {
|
||||
throw new Error('Session sharing is not configured. Please configure it in settings.');
|
||||
}
|
||||
|
||||
const config = JSON.parse(savedSessionConfig);
|
||||
if (!config.enabled || !config.baseUrl) {
|
||||
throw new Error('Session sharing is not enabled or base URL is not configured.');
|
||||
}
|
||||
|
||||
// Create a shared session
|
||||
const shareToken = await createSharedSession(
|
||||
config.baseUrl,
|
||||
session.messages,
|
||||
session.metadata.description || 'Shared Session'
|
||||
);
|
||||
|
||||
// Create the shareable link
|
||||
const shareableLink = `goose://sessions/${shareToken}`;
|
||||
setShareLink(shareableLink);
|
||||
setIsShareModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error sharing session:', error);
|
||||
setShareError(error instanceof Error ? error.message : 'Unknown error occurred');
|
||||
toast.error(
|
||||
`Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
} finally {
|
||||
setIsSharing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard
|
||||
.writeText(shareLink)
|
||||
.then(() => {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy link:', err);
|
||||
toast.error('Failed to copy link to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||
|
||||
{/* Top Row - back, info, reopen thread (fixed) */}
|
||||
<Card className="px-8 pt-6 pb-4 bg-bgSecondary flex items-center">
|
||||
<BackButton showText={false} onClick={onBack} className="text-textStandard" />
|
||||
|
||||
<SessionHeaderCard onBack={onBack}>
|
||||
{/* Session info row */}
|
||||
<div className="ml-8">
|
||||
<h1 className="text-lg font-bold text-textStandard">
|
||||
@@ -85,119 +129,83 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
onClick={onResume}
|
||||
className="ml-auto text-md cursor-pointer text-textStandard hover:font-bold hover:scale-105 transition-all duration-150"
|
||||
>
|
||||
Resume Session
|
||||
</span>
|
||||
</Card>
|
||||
<div className="ml-auto flex items-center space-x-4">
|
||||
<button
|
||||
onClick={handleShare}
|
||||
disabled={!canShare || isSharing}
|
||||
className={`flex items-center text-textStandard px-3 py-1 border rounded-md ${
|
||||
canShare
|
||||
? 'border-primary hover:text-primary hover:font-bold hover:scale-105 transition-all duration-150'
|
||||
: 'border-gray-300 cursor-not-allowed opacity-50'
|
||||
}`}
|
||||
>
|
||||
{isSharing ? (
|
||||
<>
|
||||
<LoaderCircle className="w-5 h-5 animate-spin mr-2" />
|
||||
<span>Sharing...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-120px)] w-full">
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="space-y-4 mb-6">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<div className="text-red-500 mb-4">
|
||||
<AlertCircle size={32} />
|
||||
</div>
|
||||
<p className="text-md mb-2">Error Loading Session Details</p>
|
||||
<p className="text-sm text-center mb-4">{error}</p>
|
||||
<Button onClick={onRetry} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : session?.messages?.length > 0 ? (
|
||||
session.messages
|
||||
.map((message, index) => {
|
||||
// Extract text content from the message
|
||||
const textContent = message.content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('\n');
|
||||
|
||||
// Get tool requests from the message
|
||||
const toolRequests = message.content
|
||||
.filter((c) => c.type === 'toolRequest')
|
||||
.map((c) => c as ToolRequestMessageContent);
|
||||
|
||||
// Get tool responses map using the helper function
|
||||
const toolResponsesMap = getToolResponsesMap(session, index, toolRequests);
|
||||
|
||||
// Skip pure tool response messages for cleaner display
|
||||
const isOnlyToolResponse =
|
||||
message.content.length > 0 &&
|
||||
message.content.every((c) => c.type === 'toolResponse');
|
||||
|
||||
if (message.role === 'user' && isOnlyToolResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
className={`p-4 ${
|
||||
message.role === 'user'
|
||||
? 'bg-bgSecondary border border-borderSubtle'
|
||||
: 'bg-bgSubtle'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-medium text-textStandard">
|
||||
{message.role === 'user' ? 'You' : 'Goose'}
|
||||
</span>
|
||||
<span className="text-xs text-textSubtle">
|
||||
{new Date(message.created * 1000).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Text content */}
|
||||
{textContent && (
|
||||
<div className={`${toolRequests.length > 0 ? 'mb-4' : ''}`}>
|
||||
<MarkdownContent content={textContent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool requests and responses */}
|
||||
{toolRequests.length > 0 && (
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1">
|
||||
{toolRequests.map((toolRequest) => (
|
||||
<ToolCallWithResponse
|
||||
// In the session history page, if no tool response found for given request, it means the tool call
|
||||
// is broken or cancelled.
|
||||
isCancelledMessage={
|
||||
toolResponsesMap.get(toolRequest.id) == undefined
|
||||
}
|
||||
key={toolRequest.id}
|
||||
toolRequest={toolRequest}
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
.filter(Boolean) // Filter out null entries
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<MessageSquare className="w-12 h-12 mb-4" />
|
||||
<p className="text-lg mb-2">No messages found</p>
|
||||
<p className="text-sm">This session doesn't contain any messages</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
onClick={onResume}
|
||||
className="text-md cursor-pointer text-textStandard hover:font-bold hover:scale-105 transition-all duration-150"
|
||||
>
|
||||
Resume Session
|
||||
</span>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SessionHeaderCard>
|
||||
|
||||
<SessionMessages
|
||||
messages={session.messages}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
|
||||
{/* Share Link Modal */}
|
||||
<Modal open={isShareModalOpen} onOpenChange={setIsShareModalOpen}>
|
||||
<ModalContent className="sm:max-w-md dark:bg-black">
|
||||
<ModalHeader>
|
||||
<ModalTitle className="text-textStandard">Share Session</ModalTitle>
|
||||
</ModalHeader>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 p-2 rounded-md overflow-x-auto">
|
||||
<code className="text-sm text-textStandard">{shareLink}</code>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
onClick={handleCopyLink}
|
||||
disabled={isCopied}
|
||||
>
|
||||
{isCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
<span className="sr-only">Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-textSubtle">
|
||||
Share this link with others to give them access to this session.
|
||||
<br />
|
||||
They will need to have Goose installed and session sharing configured.
|
||||
</p>
|
||||
</div>
|
||||
<ModalFooter className="sm:justify-start">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setIsShareModalOpen(false)}
|
||||
className="hover:text-textStandard border border-borderSubtle text-textStandard hover:bg-bgSubtle"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ViewConfig } from '../../App';
|
||||
import { MessageSquare, Loader, AlertCircle, Calendar, ChevronRight, Folder } from 'lucide-react';
|
||||
import {
|
||||
MessageSquare,
|
||||
LoaderCircle,
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
Folder,
|
||||
} from 'lucide-react';
|
||||
import { fetchSessions, type Session } from '../../sessions';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -80,7 +87,7 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<Loader className="h-8 w-8 animate-spin text-textPrimary" />
|
||||
<LoaderCircle className="h-8 w-8 animate-spin text-textPrimary" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-textSubtle">
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import { MessageSquare, AlertCircle } from 'lucide-react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import MarkdownContent from '../MarkdownContent';
|
||||
import ToolCallWithResponse from '../ToolCallWithResponse';
|
||||
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message';
|
||||
import { type Message } from '../../types/message';
|
||||
|
||||
/**
|
||||
* Get tool responses map from messages
|
||||
*/
|
||||
export const getToolResponsesMap = (
|
||||
messages: Message[],
|
||||
messageIndex: number,
|
||||
toolRequests: ToolRequestMessageContent[]
|
||||
) => {
|
||||
const responseMap = new Map();
|
||||
|
||||
if (messageIndex >= 0) {
|
||||
for (let i = messageIndex + 1; i < messages.length; i++) {
|
||||
const responses = messages[i].content
|
||||
.filter((c) => c.type === 'toolResponse')
|
||||
.map((c) => c as ToolResponseMessageContent);
|
||||
|
||||
for (const response of responses) {
|
||||
const matchingRequest = toolRequests.find((req) => req.id === response.id);
|
||||
if (matchingRequest) {
|
||||
responseMap.set(response.id, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responseMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the SessionHeaderCard component
|
||||
*/
|
||||
export interface SessionHeaderCardProps {
|
||||
onBack: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common header card for session views
|
||||
*/
|
||||
export const SessionHeaderCard: React.FC<SessionHeaderCardProps> = ({ onBack, children }) => {
|
||||
return (
|
||||
<Card className="px-8 pt-6 pb-4 bg-bgSecondary flex items-center">
|
||||
<BackButton showText={false} onClick={onBack} className="text-textStandard" />
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the SessionMessages component
|
||||
*/
|
||||
export interface SessionMessagesProps {
|
||||
messages: Message[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common component for displaying session messages
|
||||
*/
|
||||
export const SessionMessages: React.FC<SessionMessagesProps> = ({
|
||||
messages,
|
||||
isLoading,
|
||||
error,
|
||||
onRetry,
|
||||
}) => {
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-120px)] w-full">
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="space-y-4 mb-6">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<div className="text-red-500 mb-4">
|
||||
<AlertCircle size={32} />
|
||||
</div>
|
||||
<p className="text-md mb-2">Error Loading Session Details</p>
|
||||
<p className="text-sm text-center mb-4">{error}</p>
|
||||
<Button onClick={onRetry} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : messages?.length > 0 ? (
|
||||
messages
|
||||
.map((message, index) => {
|
||||
// Extract text content from the message
|
||||
const textContent = message.content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('\n');
|
||||
|
||||
// Get tool requests from the message
|
||||
const toolRequests = message.content
|
||||
.filter((c) => c.type === 'toolRequest')
|
||||
.map((c) => c as ToolRequestMessageContent);
|
||||
|
||||
// Get tool responses map using the helper function
|
||||
const toolResponsesMap = getToolResponsesMap(messages, index, toolRequests);
|
||||
|
||||
// Skip pure tool response messages for cleaner display
|
||||
const isOnlyToolResponse =
|
||||
message.content.length > 0 &&
|
||||
message.content.every((c) => c.type === 'toolResponse');
|
||||
|
||||
if (message.role === 'user' && isOnlyToolResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
className={`p-4 ${
|
||||
message.role === 'user'
|
||||
? 'bg-bgSecondary border border-borderSubtle'
|
||||
: 'bg-bgSubtle'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="font-medium text-textStandard">
|
||||
{message.role === 'user' ? 'You' : 'Goose'}
|
||||
</span>
|
||||
<span className="text-xs text-textSubtle">
|
||||
{new Date(message.created * 1000).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Text content */}
|
||||
{textContent && (
|
||||
<div className={`${toolRequests.length > 0 ? 'mb-4' : ''}`}>
|
||||
<MarkdownContent content={textContent} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool requests and responses */}
|
||||
{toolRequests.length > 0 && (
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 mt-1">
|
||||
{toolRequests.map((toolRequest) => (
|
||||
<ToolCallWithResponse
|
||||
// In the session history page, if no tool response found for given request, it means the tool call
|
||||
// is broken or cancelled.
|
||||
isCancelledMessage={
|
||||
toolResponsesMap.get(toolRequest.id) == undefined
|
||||
}
|
||||
key={toolRequest.id}
|
||||
toolRequest={toolRequest}
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
.filter(Boolean) // Filter out null entries
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<MessageSquare className="w-12 h-12 mb-4" />
|
||||
<p className="text-lg mb-2">No messages found</p>
|
||||
<p className="text-sm">This session doesn't contain any messages</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ViewConfig } from '../../App';
|
||||
import { fetchSessionDetails, type SessionDetails } from '../../sessions';
|
||||
import { fetchSharedSessionDetails } from '../../sharedSessions';
|
||||
import SessionListView from './SessionListView';
|
||||
import SessionHistoryView from './SessionHistoryView';
|
||||
import { Card } from '../ui/card';
|
||||
import { Input } from '../ui/input';
|
||||
import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
|
||||
interface SessionsViewProps {
|
||||
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
|
||||
@@ -70,7 +76,7 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
};
|
||||
|
||||
// If a session is selected, show the session history view
|
||||
// Otherwise, show the sessions list view
|
||||
// Otherwise, show the sessions list view with a button to test shared sessions
|
||||
return selectedSession ? (
|
||||
<SessionHistoryView
|
||||
session={selectedSession}
|
||||
@@ -81,7 +87,9 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
onRetry={handleRetryLoadSession}
|
||||
/>
|
||||
) : (
|
||||
<SessionListView setView={setView} onSelectSession={handleSelectSession} />
|
||||
<>
|
||||
<SessionListView setView={setView} onSelectSession={handleSelectSession} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Clock, Globe } from 'lucide-react';
|
||||
import { type SharedSessionDetails } from '../../sharedSessions';
|
||||
import { SessionHeaderCard, SessionMessages } from './SessionViewComponents';
|
||||
|
||||
interface SharedSessionViewProps {
|
||||
session: SharedSessionDetails | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
onBack: () => void;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
const SharedSessionView: React.FC<SharedSessionViewProps> = ({
|
||||
session,
|
||||
isLoading,
|
||||
error,
|
||||
onBack,
|
||||
onRetry,
|
||||
}) => {
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||
|
||||
{/* Top Row - back, info (fixed) */}
|
||||
<SessionHeaderCard onBack={onBack}>
|
||||
{/* Session info row */}
|
||||
<div className="ml-8">
|
||||
<h1 className="text-lg font-bold text-textStandard">
|
||||
{session ? session.description : 'Shared Session'}
|
||||
</h1>
|
||||
{session && (
|
||||
<div className="flex items-center text-sm text-textSubtle mt-2 space-x-4">
|
||||
<span className="flex items-center">
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
{new Date(session.messages[0]?.created * 1000).toLocaleString()}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Globe className="w-4 h-4 mr-1" />
|
||||
{session.base_url}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SessionHeaderCard>
|
||||
|
||||
<SessionMessages
|
||||
messages={session?.messages || []}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedSessionView;
|
||||
Reference in New Issue
Block a user