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>
  );
}