File size: 3,390 Bytes
3d700dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import ProfilesService from "#/api/profiles-service/profiles-service.api";
import { useSwitchLlmProfileAndLog } from "#/hooks/mutation/use-switch-llm-profile-and-log";
import { getLastRenderableEventId } from "#/hooks/chat/model-command-event-anchor";
import { LLM_PROFILES_QUERY_KEYS } from "#/hooks/query/query-keys";
import { I18nKey } from "#/i18n/declaration";
import { useActiveBackend } from "#/contexts/active-backend-context";
import { useModelStore } from "#/stores/model-store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { MODEL_COMMAND } from "#/utils/constants";

const MODEL_PREFIX = `${MODEL_COMMAND} `;

/**
 * Intercepts "/model" submissions (both local and cloud backends manage the
 * LLM through saved profiles):
 *   - "/model"        → render an inline list of saved profiles in the chat
 *   - "/model <name>" → switch the running conversation's LLM profile
 * Anything that isn't a "/model" command falls through to `onSubmit`.
 */
export const useModelInterceptor = (
  conversationId: string | null | undefined,
  onSubmit: (message: string) => void,
) => {
  const showProfiles = useModelStore((s) => s.show);
  const queryClient = useQueryClient();
  const { switchAndLog } = useSwitchLlmProfileAndLog();
  const { backend, orgId } = useActiveBackend();
  const { t } = useTranslation();

  return useCallback(
    (message: string) => {
      const trimmed = message.trim();
      const isModel =
        trimmed === MODEL_COMMAND || trimmed.startsWith(MODEL_PREFIX);
      if (!isModel) {
        onSubmit(message);
        return;
      }

      const arg = trimmed.slice(MODEL_COMMAND.length).trim();

      if (arg) {
        // Switch the running conversation when one is open; otherwise activate
        // the profile globally so the next conversation starts with it.
        switchAndLog(conversationId ?? null, arg);
        return;
      }

      // Bare `/model` — list profiles inline. Needs a conversation to anchor
      // the entry to; swallow silently on the home page.
      if (!conversationId) return;

      const anchorEventId = getLastRenderableEventId();

      // Imperative fetch through the query cache so the result lands on the
      // same key `useLlmProfiles` reads. `staleTime: 0` forces a fresh fetch
      // each time the user types /model.
      // Multiple rapid /model submissions intentionally append multiple chat
      // entries, matching normal command history behavior rather than replacing
      // earlier results.

      queryClient
        .fetchQuery({
          queryKey: [...LLM_PROFILES_QUERY_KEYS.all, backend.id, orgId],
          queryFn: ProfilesService.listProfiles,
          staleTime: 0,
        })
        .then(({ profiles }) =>
          showProfiles(conversationId, anchorEventId, profiles),
        )
        .catch((err: unknown) => {
          const fallback = t(I18nKey.MODEL$LIST_FAILED);
          const messageText =
            err instanceof Error && err.message ? err.message : fallback;
          displayErrorToast(messageText);
        });
    },
    [
      conversationId,
      onSubmit,
      showProfiles,
      queryClient,
      switchAndLog,
      backend.id,
      orgId,
      t,
    ],
  );
};