Skip to content

Commit 774ced8

Browse files
salevineclaude
andcommitted
feat: Add AI Settings admin page with Local LLM support and connection testing
- Add AI Settings page at /settings/ai with provider selection (Claude, OpenAI, Local LLM) - Add LOCAL_LLM enum to AIProvider - Add localLlmUrl and localLlmContextSize fields to OrganizationConfiguration - Add Test Connection button for Local LLM that validates: - URL parsing and format - DNS resolution with resolved IP display - TCP connection to host:port - TLS handshake (for HTTPS) - HTTP response and endpoint validation - Checks if response looks like an LLM API (JSON with expected fields) - Shows actual response preview from the server - Add Test Key button for Claude and OpenAI that: - Sends a real test request to verify API key works - Shows step-by-step diagnostics - Displays the AI response on success - Shows detailed error info and suggestions on failure - Fix GPT component to use styled textarea instead of missing Textarea export - Fix response interceptor handling in AI Settings page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b03edae commit 774ced8

File tree

7 files changed

+1529
-114
lines changed

7 files changed

+1529
-114
lines changed

app/client/src/ce/api/OrganizationApi.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ export interface AIConfigResponse {
2525
provider: string | null;
2626
hasClaudeApiKey: boolean;
2727
hasOpenaiApiKey: boolean;
28+
localLlmUrl?: string;
29+
localLlmContextSize?: number;
2830
}
2931

3032
export interface AIConfigRequest {
3133
claudeApiKey?: string;
3234
openaiApiKey?: string;
35+
localLlmUrl?: string;
36+
localLlmContextSize?: number;
3337
provider: string;
3438
isAIAssistantEnabled: boolean;
3539
}
@@ -85,6 +89,24 @@ export class OrganizationApi extends Api {
8589
): Promise<AxiosPromise<ApiResponse<AIConfigResponse>>> {
8690
return Api.put(`${OrganizationApi.tenantsUrl}/ai-config`, request);
8791
}
92+
93+
static async testLlmConnection(
94+
url: string,
95+
): Promise<AxiosPromise<ApiResponse<Record<string, unknown>>>> {
96+
return Api.post(`${OrganizationApi.tenantsUrl}/ai-config/test-connection`, {
97+
url,
98+
});
99+
}
100+
101+
static async testApiKey(
102+
provider: string,
103+
apiKey?: string,
104+
): Promise<AxiosPromise<ApiResponse<Record<string, unknown>>>> {
105+
return Api.post(`${OrganizationApi.tenantsUrl}/ai-config/test-api-key`, {
106+
provider,
107+
apiKey,
108+
});
109+
}
88110
}
89111

90112
export default OrganizationApi;

app/client/src/ee/components/editorComponents/GPT/index.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { EntityNavigationData } from "entities/DataTree/dataTreeTypes";
77
import React, { useState, useEffect } from "react";
88
import type CodeMirror from "codemirror";
99
import { useDispatch, useSelector } from "react-redux";
10-
import { Button, Text, Textarea } from "@appsmith/ads";
10+
import { Button, Text } from "@appsmith/ads";
1111
import styled from "styled-components";
1212
import { fetchAIResponse } from "ee/actions/aiAssistantActions";
1313
import {
@@ -92,16 +92,38 @@ const InputArea = styled.div`
9292
align-items: flex-end;
9393
`;
9494

95+
const StyledTextarea = styled.textarea`
96+
flex: 1;
97+
padding: var(--ads-v2-spaces-3);
98+
border: 1px solid var(--ads-v2-color-border);
99+
border-radius: var(--ads-v2-border-radius);
100+
background: var(--ads-v2-color-bg);
101+
color: var(--ads-v2-color-fg);
102+
font-family: inherit;
103+
font-size: var(--ads-v2-font-size-4);
104+
resize: vertical;
105+
min-height: 80px;
106+
107+
&:focus {
108+
outline: none;
109+
border-color: var(--ads-v2-color-border-emphasis);
110+
}
111+
112+
&::placeholder {
113+
color: var(--ads-v2-color-fg-muted);
114+
}
115+
`;
116+
95117
export function AIWindow(props: TAIWrapperProps) {
96118
const {
97119
children,
98-
isOpen,
99120
currentValue,
100-
update,
121+
editor,
101122
enableAIAssistance,
123+
isOpen,
102124
mode,
103-
editor,
104125
onOpenChanged,
126+
update,
105127
} = props;
106128

107129
const dispatch = useDispatch();
@@ -110,11 +132,14 @@ export function AIWindow(props: TAIWrapperProps) {
110132
const isLoading = useSelector(getIsAILoading);
111133
const error = useSelector(getAIError);
112134

113-
useEffect(function handleResponseChange() {
114-
if (lastResponse && update) {
115-
setPrompt("");
116-
}
117-
}, [lastResponse, update]);
135+
useEffect(
136+
function handleResponseChange() {
137+
if (lastResponse && update) {
138+
setPrompt("");
139+
}
140+
},
141+
[lastResponse, update],
142+
);
118143

119144
if (!enableAIAssistance || !isOpen) {
120145
return children as React.ReactElement;
@@ -166,29 +191,25 @@ export function AIWindow(props: TAIWrapperProps) {
166191
<AIWindowContent>
167192
<ResponseArea>
168193
{isLoading && <Text>Thinking...</Text>}
169-
{error && (
170-
<Text color="var(--ads-v2-color-fg-error)">{error}</Text>
171-
)}
172-
{lastResponse && !isLoading && (
173-
<Text>{lastResponse}</Text>
174-
)}
194+
{error && <Text color="var(--ads-v2-color-fg-error)">{error}</Text>}
195+
{lastResponse && !isLoading && <Text>{lastResponse}</Text>}
175196
{!lastResponse && !isLoading && !error && (
176197
<Text color="var(--ads-v2-color-fg-muted)">
177198
Ask me anything about your code...
178199
</Text>
179200
)}
180201
</ResponseArea>
181202
<InputArea>
182-
<Textarea
183-
value={prompt}
184-
onChange={(value) => setPrompt(value)}
203+
<StyledTextarea
204+
onChange={(e) => setPrompt(e.target.value)}
185205
placeholder="Describe what you want the code to do..."
186206
rows={3}
207+
value={prompt}
187208
/>
188209
<Button
210+
isLoading={isLoading}
189211
kind="primary"
190212
onClick={handleSend}
191-
isLoading={isLoading}
192213
size="md"
193214
>
194215
Send

0 commit comments

Comments
 (0)