📦 EqualifyEverything / equalify

📄 LlmSettingsInput.tsx · 159 lines
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import * as API from "aws-amplify/api";
import { useGlobalStore } from "../utils";
import { StyledLabeledInput } from "./StyledLabeledInput";
import style from "./LlmSettingsInput.module.scss";

const apiClient = API.generateClient();

interface BedrockModel {
  modelId: string;
  modelName: string;
  providerName: string;
}

const DEFAULT_MODEL_ID = "amazon.nova-lite-v1:0";

export const LlmSettingsInput = () => {
  const queryClient = useQueryClient();
  const { setAnnounceMessage } = useGlobalStore();

  const [enabled, setEnabled] = useState(true);
  const [modelId, setModelId] = useState(DEFAULT_MODEL_ID);

  const { data: options } = useQuery({
    queryKey: ["llm-settings"],
    queryFn: async () => {
      const response = await apiClient.graphql({
        query: `query {
          options(where: { key: { _in: ["llm_enabled", "llm_model_id"] } }) {
            key
            value
          }
        }`,
      });
      const rows: { key: string; value: string }[] = (response as any)?.data?.options ?? [];
      return Object.fromEntries(rows.map(({ key, value }) => [key, value]));
    },
  });

  useEffect(() => {
    if (options) {
      setEnabled(options.llm_enabled !== "false");
      setModelId(options.llm_model_id || DEFAULT_MODEL_ID);
    }
  }, [options]);

  const { data: modelsData, isLoading: isLoadingModels } = useQuery({
    queryKey: ["bedrock-models"],
    queryFn: async () => {
      return (await (
        await API.get({
          apiName: "auth",
          path: "/getBedrockModels",
          options: {},
        }).response
      ).body.json()) as unknown as { models: BedrockModel[] };
    },
  });

  const models = modelsData?.models ?? [];

  const saveMutation = useMutation({
    mutationFn: async ({ nextEnabled, nextModelId }: { nextEnabled: boolean; nextModelId: string }) => {
      await apiClient.graphql({
        query: `mutation($objects: [options_insert_input!]!) {
          insert_options(
            objects: $objects,
            on_conflict: { constraint: options_pkey, update_columns: [value] }
          ) {
            affected_rows
          }
        }`,
        variables: {
          objects: [
            { key: "llm_enabled", value: String(nextEnabled) },
            { key: "llm_model_id", value: nextModelId },
          ],
        },
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["llm-settings"] });
      setAnnounceMessage("AI summary settings saved.", "success");
    },
    onError: () => {
      setAnnounceMessage("Failed to save AI summary settings.", "error");
    },
  });

  const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
    const next = e.target.checked;
    setEnabled(next);
    saveMutation.mutate({ nextEnabled: next, nextModelId: modelId });
  };

  const handleModelChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const next = e.target.value;
    setModelId(next);
    saveMutation.mutate({ nextEnabled: enabled, nextModelId: next });
  };

  return (
    <div className={style.LlmSettingsInput}>
      <h2>LLM Blocker Summaries</h2>
      <hr />

      <StyledLabeledInput className={style.toggleRow}>
        <label htmlFor="llm-enabled">Enable LLM summaries</label>
        <input
          id="llm-enabled"
          type="checkbox"
          checked={enabled}
          onChange={handleToggle}
          className={style.toggle}
        />
      </StyledLabeledInput>
      <p className={style.description}>
        When enabled, each blocker detail page automatically generates a plain-language
        explanation of the accessibility issue and step-by-step fix instructions using
        an AWS Bedrock LLM. Summaries are cached in the database and only re-generated
        on request. Disable this to stop all LLM calls site-wide.
      </p>

      <StyledLabeledInput>
        <label htmlFor="llm-model-id">Bedrock model</label>
        {isLoadingModels ? (
          <select id="llm-model-id" disabled>
            <option>Loading models…</option>
          </select>
        ) : (
          <select
            id="llm-model-id"
            value={modelId}
            onChange={handleModelChange}
            disabled={!enabled}
            className={style.modelSelect}
          >
            {models.length === 0 && (
              <option value={DEFAULT_MODEL_ID}>{DEFAULT_MODEL_ID} (default)</option>
            )}
            {models.map(m => (
              <option key={m.modelId} value={m.modelId}>
                {m.providerName} – {m.modelName} ({m.modelId})
              </option>
            ))}
          </select>
        )}
      </StyledLabeledInput>
      <p className={style.description}>
        Choose which AWS Bedrock foundation model generates the summaries. The list shows
        models available in your region that support on-demand inference and text output.
        Changing the model takes effect immediately for new or refreshed
        summaries; existing cached summaries are not regenerated automatically.
      </p>
    </div>
  );
};