mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
8433251c86
The welcome chose the journal's words whenever the Studio had not registered as the chat's host, which also hit the Studio's own panel before (or without) that registration. A journey's chat now speaks of the journal only where the window or the phone's chat screen say they show it for its journal; everywhere else, the Studio included, it stays the book's. AssistantWindow had gone past the complexity limit choosing between the planner's trip, the journal on screen, a picked chat and the home chat; that choice now lives in windowChat.
98 lines
4.1 KiB
TypeScript
98 lines
4.1 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, journal } = useAssistantView();
|
|
// The Studio's chat is about a book, not a plan; the journey's chat shown for its journal about the journal,
|
|
// the home chat about all of the user's travels.
|
|
const about = useAssistantScope().kind;
|
|
const scope: WelcomeScope = about === 'journey' && journal ? 'journal' : about;
|
|
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">{readOnlyNote(scope, t)}</p>}
|
|
<AssistantHints view={hints} phone={variant === 'fullscreen'} />
|
|
{withSuggestions && offered.length > 0 && <AssistantSuggestions suggestions={offered} layout="stack" />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** What an empty chat is about: a trip, a journey's book in the Studio, a journal on its page, or all of them. */
|
|
type WelcomeScope = 'trip' | 'journey' | 'journal' | 'home';
|
|
|
|
/** The question over the empty chat, by what the chat is about. */
|
|
function welcomeTitle(scope: WelcomeScope, t: (key: string) => string): string {
|
|
if (scope === 'journey') return t('journey.studio.assistant.welcome');
|
|
if (scope === 'journal') return t('assistant.journal.welcome');
|
|
return scope === 'home' ? t('assistant.home.welcome') : t('assistant.empty.title');
|
|
}
|
|
|
|
/** What the empty chat tells someone who may only read. */
|
|
function readOnlyNote(scope: WelcomeScope, t: (key: string) => string): string {
|
|
if (scope === 'journey') return t('journey.studio.assistant.readOnly');
|
|
return scope === 'journal' ? t('assistant.journal.readOnly') : t('assistant.empty.readOnly');
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|