mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 01:37:50 +00:00
feat(session): 实现会话保存和加载功能
- 在 AcpConnection 和 AcpSessionManager 中添加会话保存方法 - 在 QwenAgentManager 中实现通过 ACP 和直接保存会话的功能 - 在前端添加保存会话对话框和相关交互逻辑 - 新增 QwenSessionManager 用于直接操作文件系统保存和加载会话
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
/* Save Session Dialog Styles */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: var(--app-menu-background);
|
||||
border: 1px solid var(--app-menu-border);
|
||||
border-radius: var(--corner-radius-small);
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--app-menu-border);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--app-primary-border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.form-group input.error {
|
||||
border-color: var(--vscode-inputValidation-errorBorder);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
margin-top: 6px;
|
||||
font-size: 0.9em;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 6px;
|
||||
font-size: 0.9em;
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--app-menu-border);
|
||||
}
|
||||
|
||||
.primary-button,
|
||||
.secondary-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
/* Save Session Header Button */
|
||||
.save-session-header-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.save-session-header-button:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
.save-session-header-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface SaveSessionDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (tag: string) => void;
|
||||
existingTags?: string[];
|
||||
}
|
||||
|
||||
export const SaveSessionDialog: React.FC<SaveSessionDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
existingTags = [],
|
||||
}) => {
|
||||
const [tag, setTag] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
// Focus the input when dialog opens
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Reset state when dialog closes
|
||||
setTag('');
|
||||
setError('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!tag.trim()) {
|
||||
setError('Please enter a name for this conversation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if tag already exists
|
||||
if (existingTags.includes(tag.trim())) {
|
||||
setError(
|
||||
'A conversation with this name already exists. Please choose a different name.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(tag.trim());
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dialog-overlay" onClick={onClose}>
|
||||
<div className="dialog-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dialog-header">
|
||||
<h3>Save Conversation</h3>
|
||||
<button className="dialog-close" onClick={onClose} aria-label="Close">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M12 4L4 12M4 4L12 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="dialog-body">
|
||||
<div className="form-group">
|
||||
<label htmlFor="session-tag">Conversation Name</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="session-tag"
|
||||
type="text"
|
||||
value={tag}
|
||||
onChange={(e) => {
|
||||
setTag(e.target.value);
|
||||
if (error) setError('');
|
||||
}}
|
||||
placeholder="e.g., project-planning, bug-fix, research"
|
||||
className={error ? 'error' : ''}
|
||||
/>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
<div className="form-help">
|
||||
Give this conversation a meaningful name so you can find it
|
||||
later
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dialog-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="primary-button">
|
||||
Save Conversation
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
/* Session Manager Styles */
|
||||
.session-manager {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
}
|
||||
|
||||
.session-manager-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--app-primary-border-color);
|
||||
}
|
||||
|
||||
.session-manager-header h3 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.session-manager-actions {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--app-primary-border-color);
|
||||
}
|
||||
|
||||
.session-manager-actions .secondary-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.session-manager-actions .secondary-button:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.session-search {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--app-primary-border-color);
|
||||
}
|
||||
|
||||
.session-search svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.session-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--app-primary-foreground);
|
||||
font-size: var(--vscode-chat-font-size, 13px);
|
||||
font-family: var(--vscode-chat-font-family);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.session-search input::placeholder {
|
||||
color: var(--app-input-placeholder-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.session-list-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
gap: 8px;
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.session-list-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--app-secondary-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
color: var(--app-primary-foreground);
|
||||
transition: background 0.1s ease;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background: var(--app-list-hover-background);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: var(--app-list-active-background);
|
||||
color: var(--app-list-active-foreground);
|
||||
}
|
||||
|
||||
.session-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-item-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-item-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 0.9em;
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.session-item-date,
|
||||
.session-item-count {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.session-item-actions .icon-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.session-item-actions .icon-button:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
.session-item-actions .icon-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid transparent;
|
||||
border-top: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useVSCode } from '../hooks/useVSCode.js';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdated: string;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
interface SessionManagerProps {
|
||||
currentSessionId: string | null;
|
||||
onSwitchSession: (sessionId: string) => void;
|
||||
onSaveSession: () => void;
|
||||
onResumeSession: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
export const SessionManager: React.FC<SessionManagerProps> = ({
|
||||
currentSessionId,
|
||||
onSwitchSession,
|
||||
onSaveSession,
|
||||
onResumeSession,
|
||||
}) => {
|
||||
const vscode = useVSCode();
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Load sessions when component mounts
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
const loadSessions = React.useCallback(() => {
|
||||
setIsLoading(true);
|
||||
vscode.postMessage({
|
||||
type: 'listSavedSessions',
|
||||
data: {},
|
||||
});
|
||||
}, [vscode]);
|
||||
|
||||
// Listen for session list updates
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.type === 'savedSessionsList') {
|
||||
setIsLoading(false);
|
||||
setSessions(message.data.sessions || []);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, []);
|
||||
|
||||
const filteredSessions = sessions.filter((session) =>
|
||||
session.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleSaveCurrent = () => {
|
||||
onSaveSession();
|
||||
};
|
||||
|
||||
const handleResumeSession = (sessionId: string) => {
|
||||
onResumeSession(sessionId);
|
||||
};
|
||||
|
||||
const handleSwitchSession = (sessionId: string) => {
|
||||
onSwitchSession(sessionId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-manager">
|
||||
<div className="session-manager-header">
|
||||
<h3>Saved Conversations</h3>
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={loadSessions}
|
||||
disabled={isLoading}
|
||||
title="Refresh sessions"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M13.3333 8C13.3333 10.9455 10.9455 13.3333 8 13.3333C5.05451 13.3333 2.66663 10.9455 2.66663 8C2.66663 5.05451 5.05451 2.66663 8 2.66663"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.6666 8L13.3333 8M13.3333 8L13.3333 5.33333M13.3333 8L10.6666 10.6667"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="session-manager-actions">
|
||||
<button className="secondary-button" onClick={handleSaveCurrent}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M2.66663 2.66663H10.6666L13.3333 5.33329V13.3333H2.66663V2.66663Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 10.6666V8M8 8V5.33329M8 8H10.6666M8 8H5.33329"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Save Current
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="session-search">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M7.33329 12.6666C10.2788 12.6666 12.6666 10.2788 12.6666 7.33329C12.6666 4.38777 10.2788 2 7.33329 2C4.38777 2 2 4.38777 2 7.33329C2 10.2788 4.38777 12.6666 7.33329 12.6666Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.9999 14L11.0999 11.1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search conversations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="session-list">
|
||||
{isLoading ? (
|
||||
<div className="session-list-loading">
|
||||
<div className="loading-spinner"></div>
|
||||
<span>Loading conversations...</span>
|
||||
</div>
|
||||
) : filteredSessions.length === 0 ? (
|
||||
<div className="session-list-empty">
|
||||
{searchQuery
|
||||
? 'No matching conversations'
|
||||
: 'No saved conversations yet'}
|
||||
</div>
|
||||
) : (
|
||||
filteredSessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`session-item ${session.id === currentSessionId ? 'active' : ''}`}
|
||||
>
|
||||
<div className="session-item-info">
|
||||
<div className="session-item-name">{session.name}</div>
|
||||
<div className="session-item-meta">
|
||||
<span className="session-item-date">
|
||||
{new Date(session.lastUpdated).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="session-item-count">
|
||||
{session.messageCount}{' '}
|
||||
{session.messageCount === 1 ? 'message' : 'messages'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="session-item-actions">
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={() => handleResumeSession(session.id)}
|
||||
title="Resume this conversation"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M5.33337 4L10.6667 8L5.33337 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={() => handleSwitchSession(session.id)}
|
||||
title="Switch to this conversation"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M10.6666 4L13.3333 6.66667L10.6666 9.33333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.66663 6.66667H13.3333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user