mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
5d334d7ec8
A third scope, home, next to trip and journey: threads that name neither a trip nor a journey (the CHECK now only forbids both), reached under /api/assistant by their owner only. A home turn reads the user's trips through list_trips into a short card (under way, next, later, behind, undated), offers every read (a trip's on the id the model names, checked by the tool with the user's rights) and only the writes that take no trip or journey (atlas, vacation planner, collections, tour library, settings), all held as change cards. open_trip_chat answers with a card that goes on in that trip's own chat with the question waiting in its composer. On the desktop the window opens on the home chat outside a trip; its head lists the trips to switch into and a switched trip chat has the way back. On the phone the home chat opens full screen from the dashboard head and the dock's More list, and the card opens the trip with its chat. Both run on useAssistantHome.
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
import { ThreadPrimitive } from '@assistant-ui/react';
|
|
|
|
import { useTranslation } from '../../i18n';
|
|
import EmptyState from '../shared/EmptyState';
|
|
import AssistantHints, { MAX_VISIBLE_HINTS } from './AssistantHints';
|
|
import { useAssistantScope, useAssistantView } from './assistantViewContext';
|
|
import { useAssistantHints } from './useAssistantHints';
|
|
import type { ReadySuggestion } from './useAssistantSuggestions';
|
|
|
|
/** Suggestions left under a full list of hints, so both fit the smallest window. */
|
|
const SUGGESTIONS_UNDER_HINTS = 2;
|
|
|
|
/**
|
|
* The empty chat: the mascot, one question, and a few things to ask that fit
|
|
* where the trip stands. A suggestion sends itself; there is nothing to edit
|
|
* first. On the phone the suggestions sit by the composer instead, where the
|
|
* thumb is, so this shows them only when `withSuggestions` asks for it.
|
|
* Between the two sit the plan hints, when the trip has any.
|
|
*/
|
|
export default function AssistantWelcome({
|
|
suggestions,
|
|
canEdit,
|
|
withSuggestions,
|
|
}: {
|
|
suggestions: ReadySuggestion[];
|
|
canEdit: boolean;
|
|
withSuggestions: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { tripId, variant } = useAssistantView();
|
|
// The Studio's chat is about a book, not a plan; the home chat about all of the user's travels.
|
|
const scope = useAssistantScope().kind;
|
|
const book = scope === 'journey';
|
|
const hints = useAssistantHints(tripId, { withStays: true });
|
|
const crowded = Math.min(hints.hints.length, MAX_VISIBLE_HINTS) > SUGGESTIONS_UNDER_HINTS;
|
|
const offered = crowded ? suggestions.slice(0, SUGGESTIONS_UNDER_HINTS) : suggestions;
|
|
return (
|
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 py-6">
|
|
<EmptyState scene="chat" size={72} compact surface="var(--bg-elevated)" title={welcomeTitle(scope, t)} />
|
|
{!canEdit && (
|
|
<p className="m-0 max-w-xs text-center text-caption text-content-muted">
|
|
{book ? t('journey.studio.assistant.readOnly') : t('assistant.empty.readOnly')}
|
|
</p>
|
|
)}
|
|
<AssistantHints view={hints} phone={variant === 'fullscreen'} />
|
|
{withSuggestions && offered.length > 0 && <AssistantSuggestions suggestions={offered} layout="stack" />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** The question over the empty chat, by what the chat is about. */
|
|
function welcomeTitle(scope: 'trip' | 'journey' | 'home', t: (key: string) => string): string {
|
|
if (scope === 'journey') return t('journey.studio.assistant.welcome');
|
|
return scope === 'home' ? t('assistant.home.welcome') : t('assistant.empty.title');
|
|
}
|
|
|
|
export function AssistantSuggestions({
|
|
suggestions,
|
|
layout,
|
|
}: {
|
|
suggestions: ReadySuggestion[];
|
|
/** 'stack' under the mascot, 'row' as a strip of chips over the composer. */
|
|
layout: 'stack' | 'row';
|
|
}) {
|
|
return (
|
|
<div
|
|
className={
|
|
layout === 'stack'
|
|
? 'trek-stagger flex w-full max-w-sm flex-col gap-1.5'
|
|
: 'trek-stagger -mx-1 flex gap-2 overflow-x-auto px-1 pb-0.5 [scrollbar-width:none]'
|
|
}
|
|
>
|
|
{suggestions.map((s) => (
|
|
<ThreadPrimitive.Suggestion
|
|
key={s.id}
|
|
prompt={s.prompt}
|
|
send
|
|
dir="auto"
|
|
className={
|
|
layout === 'stack'
|
|
? 'rounded-xl border border-edge-faint bg-surface-card px-3.5 py-2.5 text-start text-body text-content transition-colors hover:border-edge hover:bg-surface-hover'
|
|
: 'min-h-[44px] flex-shrink-0 whitespace-nowrap rounded-full border border-edge-faint bg-surface-card px-4 text-body text-content'
|
|
}
|
|
>
|
|
{s.prompt}
|
|
</ThreadPrimitive.Suggestion>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|