mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-05 12:25:35 +08:00
Compare commits
3 Commits
fix/plugin
...
fix/chat-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cccc1ad268 | ||
|
|
c825d342b3 | ||
|
|
d9e9df183f |
17
package-lock.json
generated
17
package-lock.json
generated
@@ -39,6 +39,7 @@
|
|||||||
"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",
|
||||||
@@ -4580,6 +4581,13 @@
|
|||||||
"@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",
|
||||||
@@ -7485,6 +7493,15 @@
|
|||||||
"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,6 +96,7 @@
|
|||||||
"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,6 +143,21 @@ 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,
|
||||||
@@ -204,7 +219,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('none');
|
const [thinkingMode, setThinkingMode] = useState(getInitialThinkingMode);
|
||||||
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -564,7 +579,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 (selectedThinkingMode && selectedThinkingMode.prefix) {
|
if (provider === 'claude' && selectedThinkingMode && selectedThinkingMode.prefix) {
|
||||||
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,7 +764,6 @@ 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';
|
||||||
@@ -795,6 +809,10 @@ 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,4 +1,6 @@
|
|||||||
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 = {
|
||||||
@@ -10,6 +12,48 @@ 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)}`
|
||||||
@@ -24,9 +68,11 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
return r.text();
|
return r.text();
|
||||||
})
|
})
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
if (text && text.trimStart().startsWith('<svg')) {
|
if (!text) return;
|
||||||
svgCache.set(url, text);
|
const sanitized = sanitizeSvg(text);
|
||||||
setSvg(text);
|
if (sanitized) {
|
||||||
|
svgCache.set(url, sanitized);
|
||||||
|
setSvg(sanitized);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -35,10 +81,6 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
if (!svg) return <span className={className} />;
|
if (!svg) return <span className={className} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={className} dangerouslySetInnerHTML={{ __html: svg }} />
|
||||||
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