File size: 4,800 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | import React from "react";
import { Trans } from "react-i18next";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useModelStore } from "#/stores/model-store";
import { I18nKey } from "#/i18n/declaration";
import InfoCircleIcon from "#/icons/info-circle.svg?react";
import type { ProfileInfo } from "#/api/profiles-service/profiles-service.api";
import { useFreeModels } from "#/hooks/query/use-free-models";
import { formatModelNameForDisplay } from "#/utils/format-model-name";
import { GenericEventMessage } from "./generic-event-message";
interface ProfileRowProps {
profile: ProfileInfo;
}
function ProfileRow({ profile }: ProfileRowProps) {
const [expanded, setExpanded] = React.useState(false);
const freeModels = useFreeModels();
const displayModel = formatModelNameForDisplay(profile.model, freeModels);
return (
<div className="border border-neutral-700 rounded-md overflow-hidden">
<button
type="button"
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
aria-label={`Toggle details for ${profile.name}`}
className="w-full py-1.5 px-2 text-left flex items-center gap-2 hover:bg-neutral-700 transition-colors cursor-pointer"
>
<span className="text-neutral-300">
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
<span className="font-normal text-neutral-200 text-sm">
{profile.name}
</span>
</button>
{expanded && (
<>
<hr className="border-neutral-700" />
<div className="px-3 py-2 text-xs text-neutral-300 font-mono whitespace-pre-wrap">
{`model: ${displayModel ?? "—"}\n` +
`base_url: ${profile.base_url ?? "—"}\n` +
// eslint-disable-next-line i18next/no-literal-string -- diagnostic readout; field labels are intentionally untranslated
`api_key: ${profile.api_key_set ? "set" : "not set"}`}
</div>
</>
)}
</div>
);
}
export interface ModelMessagesProps {
conversationId: string | null | undefined;
/**
* Render only entries anchored to this event id. Use `null` to render the
* "no events at the time of /model" entries (top of the chat history).
*/
anchorEventId: string | null;
}
export function ModelMessages({
conversationId,
anchorEventId,
}: ModelMessagesProps) {
const entriesById = useModelStore((s) => s.entriesByConversation);
const allEntries = conversationId ? (entriesById[conversationId] ?? []) : [];
const entries = allEntries.filter((e) => e.anchorEventId === anchorEventId);
if (!conversationId || entries.length === 0) return null;
return (
<div data-testid="model-messages" className="flex flex-col w-full">
{entries.map((entry) => {
if (entry.switchedTo) {
return (
<GenericEventMessage
key={entry.id}
title={
<span className="inline-flex items-center gap-1.5">
<InfoCircleIcon
width={14}
height={14}
className="shrink-0 text-neutral-400"
aria-hidden
/>
<Trans
i18nKey={I18nKey.MODEL$SWITCHED_TO_PROFILE}
values={{ name: entry.switchedTo }}
components={{
cmd: (
<span className="font-mono text-neutral-200 bg-neutral-800 px-1 rounded" />
),
}}
/>
</span>
}
details=""
/>
);
}
const isEmpty = entry.profiles.length === 0;
return (
<GenericEventMessage
key={entry.id}
title={
<span>
{isEmpty ? (
<Trans i18nKey={I18nKey.MODEL$NO_SAVED_PROFILES} />
) : (
<Trans
i18nKey={I18nKey.MODEL$AVAILABLE_PROFILES}
values={{ count: entry.profiles.length }}
/>
)}
</span>
}
details={
isEmpty ? (
<span className="text-neutral-300 text-sm px-2 py-1 block">
<Trans i18nKey={I18nKey.MODEL$NO_PROFILES_HINT} />
</span>
) : (
<div className="flex flex-col gap-1 mt-1">
{entry.profiles.map((p) => (
<ProfileRow key={p.name} profile={p} />
))}
</div>
)
}
initiallyExpanded={isEmpty}
/>
);
})}
</div>
);
}
|