mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-07 05:45:39 +08:00
Compare commits
1 Commits
fix/chat-t
...
fix/claude
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a87214050b |
17
package-lock.json
generated
17
package-lock.json
generated
@@ -39,7 +39,6 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"dompurify": "^3.4.7",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
@@ -4581,13 +4580,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
|
||||||
"version": "2.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/unist": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
@@ -7493,15 +7485,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
|
||||||
"version": "3.4.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
|
|
||||||
"integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==",
|
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@types/trusted-types": "^2.0.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dot-prop": {
|
"node_modules/dot-prop": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
||||||
|
|||||||
@@ -96,7 +96,6 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"dompurify": "^3.4.7",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
|||||||
@@ -143,21 +143,6 @@ const createFakeSubmitEvent = () => {
|
|||||||
return { preventDefault: () => undefined } as unknown as FormEvent<HTMLFormElement>;
|
return { preventDefault: () => undefined } as unknown as FormEvent<HTMLFormElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const THINKING_MODE_STORAGE_KEY = 'chat-thinking-mode';
|
|
||||||
|
|
||||||
const getInitialThinkingMode = () => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedMode = safeLocalStorage.getItem(THINKING_MODE_STORAGE_KEY);
|
|
||||||
if (!savedMode) {
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
return thinkingModes.some((mode) => mode.id === savedMode) ? savedMode : 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNotificationSessionSummary = (
|
const getNotificationSessionSummary = (
|
||||||
selectedSession: ProjectSession | null,
|
selectedSession: ProjectSession | null,
|
||||||
fallbackInput: string,
|
fallbackInput: string,
|
||||||
@@ -219,7 +204,7 @@ export function useChatComposerState({
|
|||||||
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
||||||
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
||||||
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
||||||
const [thinkingMode, setThinkingMode] = useState(getInitialThinkingMode);
|
const [thinkingMode, setThinkingMode] = useState('none');
|
||||||
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -579,7 +564,7 @@ export function useChatComposerState({
|
|||||||
|
|
||||||
let messageContent = currentInput;
|
let messageContent = currentInput;
|
||||||
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
||||||
if (provider === 'claude' && selectedThinkingMode && selectedThinkingMode.prefix) {
|
if (selectedThinkingMode && selectedThinkingMode.prefix) {
|
||||||
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,6 +749,7 @@ export function useChatComposerState({
|
|||||||
setUploadingImages(new Map());
|
setUploadingImages(new Map());
|
||||||
setImageErrors(new Map());
|
setImageErrors(new Map());
|
||||||
setIsTextareaExpanded(false);
|
setIsTextareaExpanded(false);
|
||||||
|
setThinkingMode('none');
|
||||||
|
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
@@ -809,10 +795,6 @@ export function useChatComposerState({
|
|||||||
inputValueRef.current = input;
|
inputValueRef.current = input;
|
||||||
}, [input]);
|
}, [input]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
safeLocalStorage.setItem(THINKING_MODE_STORAGE_KEY, thinkingMode);
|
|
||||||
}, [thinkingMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProjectId) {
|
if (!selectedProjectId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,48 +10,6 @@ type Props = {
|
|||||||
// Module-level cache so repeated renders don't re-fetch
|
// Module-level cache so repeated renders don't re-fetch
|
||||||
const svgCache = new Map<string, string>();
|
const svgCache = new Map<string, string>();
|
||||||
|
|
||||||
const FORBIDDEN_SVG_TAGS = [
|
|
||||||
'script',
|
|
||||||
'foreignObject',
|
|
||||||
'iframe',
|
|
||||||
'object',
|
|
||||||
'embed',
|
|
||||||
'link',
|
|
||||||
'meta',
|
|
||||||
'style',
|
|
||||||
'animate',
|
|
||||||
'set',
|
|
||||||
'animateTransform',
|
|
||||||
'animateMotion',
|
|
||||||
];
|
|
||||||
|
|
||||||
const FORBIDDEN_SVG_ATTRS = [
|
|
||||||
'href',
|
|
||||||
'xlink:href',
|
|
||||||
'src',
|
|
||||||
'style',
|
|
||||||
];
|
|
||||||
|
|
||||||
function sanitizeSvg(svgText: string): string | null {
|
|
||||||
const sanitized = DOMPurify.sanitize(svgText, {
|
|
||||||
USE_PROFILES: { svg: true, svgFilters: true },
|
|
||||||
FORBID_TAGS: FORBIDDEN_SVG_TAGS,
|
|
||||||
FORBID_ATTR: FORBIDDEN_SVG_ATTRS,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sanitized) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const doc = new DOMParser().parseFromString(sanitized, 'image/svg+xml');
|
|
||||||
const root = doc.documentElement;
|
|
||||||
if (!root || root.nodeName.toLowerCase() !== 'svg') return null;
|
|
||||||
if (doc.querySelector('parsererror')) return null;
|
|
||||||
return sanitized;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
||||||
const url = iconFile
|
const url = iconFile
|
||||||
? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`
|
? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`
|
||||||
@@ -68,11 +24,9 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
return r.text();
|
return r.text();
|
||||||
})
|
})
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
if (!text) return;
|
if (text && text.trimStart().startsWith('<svg')) {
|
||||||
const sanitized = sanitizeSvg(text);
|
svgCache.set(url, text);
|
||||||
if (sanitized) {
|
setSvg(text);
|
||||||
svgCache.set(url, sanitized);
|
|
||||||
setSvg(sanitized);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -81,6 +35,10 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
if (!svg) return <span className={className} />;
|
if (!svg) return <span className={className} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={className} dangerouslySetInnerHTML={{ __html: svg }} />
|
<span
|
||||||
|
className={className}
|
||||||
|
// SVG is fetched from the user's own installed plugin — same trust level as the plugin code itself
|
||||||
|
dangerouslySetInnerHTML={{ __html: svg }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user