mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 00:16:20 +00:00
feat: air-gapped deployment guide, no implicit downloads
- Ship tiktoken's cl100k_base inside the package and build the encoding from it, so token counting never downloads anything. - Default EMBEDDINGS_CACHE_DIR to <data home>/models instead of FastEmbed's temp dir, and read tokenizer.json and repo metadata from that cache, so a model downloads once and survives reboots. - TTS_PROVIDER=none and STT_PROVIDER=none switch the speech features off: the endpoints return 404, audio files fail to ingest with a clear message, /api/config reports tts_available/stt_available, and the UI hides the Speak and microphone buttons. - Drop the Google Fonts Roboto import from the web UI. - prefetch-models fills the cache the app reads; verify-offline checks the packaged encoding. - Docs: new Air-Gapped Deployment guide, settings and cache notes.
This commit is contained in:
1 parent
ca69d1ea29
commit
7da46c2bea
38 files changed
+100892
-95
No files matched your search
@@ -30,6 +30,7 @@ import {
|
||||
selectSelectedDocs,
|
||||
selectSelectedModel,
|
||||
selectSourceDocs,
|
||||
selectSttAvailable,
|
||||
selectToken,
|
||||
setSelectedDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
@@ -348,6 +349,7 @@ export default function MessageInput({
|
||||
const token = useSelector(selectToken);
|
||||
const attachments = useSelector(selectAttachments);
|
||||
const selectedModel = useSelector(selectSelectedModel);
|
||||
const sttAvailable = useSelector(selectSttAvailable);
|
||||
const unreadableAttachmentIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
@@ -1771,7 +1773,7 @@ export default function MessageInput({
|
||||
loading={toolsLoading}
|
||||
/>
|
||||
)}
|
||||
{ENABLE_VOICE_INPUT && (
|
||||
{ENABLE_VOICE_INPUT && sttAvailable && (
|
||||
<MicButton
|
||||
recordingState={recordingState}
|
||||
loading={loading}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
selectChunks,
|
||||
selectSelectedDocs,
|
||||
selectToken,
|
||||
selectTtsAvailable,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import { isToolCallRunning } from '../utils/streamingStatusUtils';
|
||||
import AnswerFlow from './AnswerFlow';
|
||||
@@ -110,6 +111,7 @@ const ConversationBubble = forwardRef<
|
||||
// const bubbleRef = useRef<HTMLDivElement | null>(null);
|
||||
const chunks = useSelector(selectChunks);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const ttsAvailable = useSelector(selectTtsAvailable);
|
||||
const [isEditClicked, setIsEditClicked] = useState(false);
|
||||
const [editInputBox, setEditInputBox] = useState<string>('');
|
||||
const messageRef = useRef<HTMLDivElement>(null);
|
||||
@@ -554,9 +556,11 @@ const ConversationBubble = forwardRef<
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative block items-center justify-center">
|
||||
<SpeakButton text={message} />
|
||||
</div>
|
||||
{ttsAvailable && (
|
||||
<div className="relative block items-center justify-center">
|
||||
<SpeakButton text={message} />
|
||||
</div>
|
||||
)}
|
||||
{handleFeedback && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center">
|
||||
|
||||
@@ -81,6 +81,8 @@ const makeStore = (
|
||||
agentFolders: null,
|
||||
roles: [],
|
||||
rolesResolved: false,
|
||||
ttsAvailable: true,
|
||||
sttAvailable: true,
|
||||
};
|
||||
const conversation: ConversationState = {
|
||||
queries: [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import {
|
||||
getDocs,
|
||||
getConversations,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
setConversations,
|
||||
setPrompts,
|
||||
setSourceDocs,
|
||||
setSpeechAvailability,
|
||||
} from '../preferences/preferenceSlice';
|
||||
|
||||
/**
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
* - Fetching and setting up source documents
|
||||
* - Fetching and setting up prompts
|
||||
* - Fetching and setting up conversations
|
||||
* - Reading which speech features the server has switched on
|
||||
*
|
||||
* @param isAuthLoading -
|
||||
*/
|
||||
@@ -30,6 +33,22 @@ export default function useDataInitializer(isAuthLoading: boolean) {
|
||||
const token = useSelector(selectToken);
|
||||
const conversations = useSelector(selectConversations);
|
||||
|
||||
// Speech features; /api/config needs no auth.
|
||||
useEffect(() => {
|
||||
userService
|
||||
.getConfig()
|
||||
.then((response) => response.json())
|
||||
.then((config) => {
|
||||
dispatch(
|
||||
setSpeechAvailability({
|
||||
tts: config?.tts_available !== false,
|
||||
stt: config?.stt_available !== false,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [dispatch]);
|
||||
|
||||
// Initialize documents
|
||||
useEffect(() => {
|
||||
// Skip if auth is still loading
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap')
|
||||
layer(base);
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import 'tw-animate-css';
|
||||
@@ -8,7 +5,6 @@ layer(base);
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--font-roboto: Roboto, sans-serif;
|
||||
--font-mono:
|
||||
'IBMPlexMono-Medium', ui-monospace, SFMono-Regular, Menlo, Consolas,
|
||||
monospace;
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function AgentDetailsModal({
|
||||
</div>
|
||||
{sharedToken ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-roboto dark:text-foreground inline text-sm leading-normal font-medium break-all text-gray-700">
|
||||
<p className="dark:text-foreground inline text-sm leading-normal font-medium break-all text-gray-700">
|
||||
<a
|
||||
href={`${baseURL}/shared/agent/${sharedToken}`}
|
||||
target="_blank"
|
||||
@@ -172,7 +172,7 @@ export default function AgentDetailsModal({
|
||||
{apiKey ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-roboto dark:text-foreground text-sm leading-normal font-medium break-all text-gray-700">
|
||||
<div className="dark:text-foreground text-sm leading-normal font-medium break-all text-gray-700">
|
||||
{apiKey}
|
||||
{!apiKey.includes('...') && (
|
||||
<CopyButton
|
||||
@@ -231,7 +231,7 @@ export default function AgentDetailsModal({
|
||||
</div>
|
||||
{webhookUrl ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-roboto dark:text-foreground text-sm leading-normal font-medium break-all text-gray-700">
|
||||
<p className="dark:text-foreground text-sm leading-normal font-medium break-all text-gray-700">
|
||||
<a href={webhookUrl} target="_blank" rel="noreferrer">
|
||||
{webhookUrl}
|
||||
</a>
|
||||
|
||||
@@ -44,6 +44,10 @@ export interface Preference {
|
||||
// not act before the first /me response (avoids a first-paint admin bounce).
|
||||
roles: string[];
|
||||
rolesResolved: boolean;
|
||||
// Speech features the server has switched on (/api/config). True until the
|
||||
// config loads, so a backend without these flags keeps showing the controls.
|
||||
ttsAvailable: boolean;
|
||||
sttAvailable: boolean;
|
||||
}
|
||||
|
||||
const initialState: Preference = {
|
||||
@@ -74,6 +78,8 @@ const initialState: Preference = {
|
||||
agentFolders: null,
|
||||
roles: [],
|
||||
rolesResolved: false,
|
||||
ttsAvailable: true,
|
||||
sttAvailable: true,
|
||||
};
|
||||
|
||||
export const prefSlice = createSlice({
|
||||
@@ -142,6 +148,13 @@ export const prefSlice = createSlice({
|
||||
state.roles = [];
|
||||
state.rolesResolved = false;
|
||||
},
|
||||
setSpeechAvailability: (
|
||||
state,
|
||||
action: PayloadAction<{ tts: boolean; stt: boolean }>,
|
||||
) => {
|
||||
state.ttsAvailable = action.payload.tts;
|
||||
state.sttAvailable = action.payload.stt;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -166,6 +179,7 @@ export const {
|
||||
setAgentFolders,
|
||||
setRoles,
|
||||
clearRoles,
|
||||
setSpeechAvailability,
|
||||
} = prefSlice.actions;
|
||||
export default prefSlice.reducer;
|
||||
|
||||
@@ -314,3 +328,7 @@ export const selectRolesResolved = (state: RootState) =>
|
||||
state.preference.rolesResolved;
|
||||
export const selectIsAdmin = (state: RootState) =>
|
||||
state.preference.roles.includes('admin');
|
||||
export const selectTtsAvailable = (state: RootState) =>
|
||||
state.preference.ttsAvailable;
|
||||
export const selectSttAvailable = (state: RootState) =>
|
||||
state.preference.sttAvailable;
|
||||
@@ -56,6 +56,8 @@ const preloadedState: { preference: Preference } = {
|
||||
agentFolders: null,
|
||||
roles: [],
|
||||
rolesResolved: false,
|
||||
ttsAvailable: true,
|
||||
sttAvailable: true,
|
||||
},
|
||||
};
|
||||
const store = configureStore({
|
||||
|
||||
Reference in new issue
Block a user