Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions src/webview/components/ClusterCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ interface ClusterCardProps {
isNoise?: boolean;
tags?: string[];
onRename?: (newName: string) => void;
onNoteDrop?: (noteIndex: number) => void;
}

export const ClusterCard: React.FC<ClusterCardProps> = ({ title, noteIndices, notes, isNoise, tags, onRename }) => {
export const ClusterCard: React.FC<ClusterCardProps> = ({
title,
noteIndices,
notes,
isNoise,
tags,
onRename,
onNoteDrop,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const [isEditing, setIsEditing] = React.useState(false);
const [editValue, setEditValue] = React.useState(title);
const [isDragOver, setIsDragOver] = React.useState(false);

React.useEffect(() => {
setEditValue(title);
Expand Down Expand Up @@ -58,7 +68,30 @@ export const ClusterCard: React.FC<ClusterCardProps> = ({ title, noteIndices, no
const countLabel = count === 1 ? '1 note' : `${count} notes`;

return (
<div className={`cluster-card${isNoise ? ' noise' : ''}${isExpanded ? ' expanded' : ''}`}>
<div
className={`cluster-card${isNoise ? ' noise' : ''}${isExpanded ? ' expanded' : ''}${isDragOver ? ' drag-over' : ''}`}
onDragOver={(e) => {
e.preventDefault();
}}
onDragEnter={(e) => {
e.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => {
setIsDragOver(false);
}}
onDrop={(e) => {
e.preventDefault();
setIsDragOver(false);
const noteIndexStr = e.dataTransfer.getData('text/plain');
if (noteIndexStr) {
const noteIndex = parseInt(noteIndexStr, 10);
if (!isNaN(noteIndex) && onNoteDrop) {
onNoteDrop(noteIndex);
}
}
}}
>
<div className="cluster-header" onClick={handleHeaderClick}>
<div className="cluster-header-left">
{isEditing ? (
Expand Down Expand Up @@ -112,7 +145,15 @@ export const ClusterCard: React.FC<ClusterCardProps> = ({ title, noteIndices, no
const note = notes[idx];
if (!note) return null;
return (
<div key={note.noteId} className="note-item" onClick={() => handleNoteClick(note.noteId)}>
<div
key={note.noteId}
className="note-item"
onClick={() => handleNoteClick(note.noteId)}
draggable="true"
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', idx.toString());
}}
>
<svg
width="13"
height="13"
Expand Down
62 changes: 62 additions & 0 deletions src/webview/context/AppStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface AppStateContextType {
changeStrategy: (index: number) => void;
setView: (view: ViewType) => void;
updateClusterName: (clusterId: number, newName: string) => void;
moveNoteToCluster: (noteIndex: number, targetClusterId: number) => void;
addCluster: (name: string) => boolean;

// settings states
settings: {
Expand Down Expand Up @@ -290,6 +292,64 @@ export const AppStateProvider: React.FC<{ children: React.ReactNode }> = ({ chil
});
};

const moveNoteToCluster = (noteIndex: number, targetClusterId: number) => {
setStrategies((prev) => {
const next = [...prev];
if (next[selectedStrategyIndex]) {
const strat = { ...next[selectedStrategyIndex] };
const newAssignments = [...strat.assignments];

if (noteIndex < 0 || noteIndex >= newAssignments.length) return prev;
if (newAssignments[noteIndex] === targetClusterId) return prev;

newAssignments[noteIndex] = targetClusterId;
strat.assignments = newAssignments;
next[selectedStrategyIndex] = strat;
}
return next;
});
};

const addCluster = (name: string): boolean => {
const trimmedName = name.trim();
if (!trimmedName) return false;

const currentStrategy = strategies[selectedStrategyIndex];
if (!currentStrategy) return false;

const clusterNames = currentStrategy.clusterNames || {};
const nameExists = Object.values(clusterNames).some((n) => n.toLowerCase() === trimmedName.toLowerCase());

if (nameExists) {
return false;
}

setStrategies((prev) => {
const next = [...prev];
const strat = next[selectedStrategyIndex];
if (strat) {
const newStrat = { ...strat };
const newClusterNames = { ...strat.clusterNames };
const newTags = { ...strat.tags };

const clusterIds = Object.keys(newClusterNames).map(Number);
const newClusterId = clusterIds.length > 0 ? Math.max(...clusterIds) + 1 : 0;

newClusterNames[newClusterId] = trimmedName;
newTags[newClusterId] = [];

newStrat.clusterNames = newClusterNames;
newStrat.tags = newTags;
newStrat.clusterCount = (newStrat.clusterCount || 0) + 1;

next[selectedStrategyIndex] = newStrat;
}
return next;
});

return true;
};

const updateSetting = async (key: string, value: any) => {
try {
await webviewApi.postMessage({
Expand Down Expand Up @@ -391,6 +451,8 @@ export const AppStateProvider: React.FC<{ children: React.ReactNode }> = ({ chil
changeStrategy,
setView,
updateClusterName,
moveNoteToCluster,
addCluster,
settings,
updateSetting,
fetchSettings,
Expand Down
101 changes: 92 additions & 9 deletions src/webview/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const DashboardPage: React.FC = () => {
changeStrategy,
notes,
updateClusterName,
moveNoteToCluster,
addCluster,
isApplying,
applyProgress,
applyError,
Expand All @@ -24,10 +26,19 @@ export const DashboardPage: React.FC = () => {

const selectedStrategy = strategies[selectedStrategyIndex];

const [isAddingCluster, setIsAddingCluster] = React.useState(false);
const [newClusterName, setNewClusterName] = React.useState('');
const [duplicateError, setDuplicateError] = React.useState(false);

const clusters: { [key: number]: number[] } = {};
const noise: number[] = [];

if (selectedStrategy) {
const clusterNames = selectedStrategy.clusterNames || {};
Object.keys(clusterNames).forEach((clusterId) => {
clusters[Number(clusterId)] = [];
});

selectedStrategy.assignments.forEach((clusterId, noteIndex) => {
if (clusterId === -1) {
noise.push(noteIndex);
Expand All @@ -44,6 +55,21 @@ export const DashboardPage: React.FC = () => {
.map(Number)
.sort((a, b) => clusters[b].length - clusters[a].length);

const handleAddClusterSubmit = (e?: React.FormEvent) => {
if (e) e.preventDefault();
const name = newClusterName.trim();
if (name) {
const success = addCluster(name);
if (success) {
setNewClusterName('');
setIsAddingCluster(false);
setDuplicateError(false);
} else {
setDuplicateError(true);
}
}
};

const handleApply = () => {
applyChanges({
method: 'both',
Expand All @@ -62,18 +88,75 @@ export const DashboardPage: React.FC = () => {
/>

<div className="cluster-list visible">
{sortedClusterIds.map((id) => (
{selectedStrategy &&
sortedClusterIds.map((id) => (
<ClusterCard
key={id}
title={selectedStrategy.clusterNames?.[id] || `Cluster ${id + 1}`}
noteIndices={clusters[id]}
notes={notes}
tags={selectedStrategy.tags?.[id]}
onRename={(newName) => updateClusterName(id, newName)}
onNoteDrop={(noteIndex) => moveNoteToCluster(noteIndex, id)}
/>
))}

{selectedStrategy && (
<div className={`cluster-card add-cluster-card${isAddingCluster ? ' editing' : ''}`}>
{isAddingCluster ? (
<form onSubmit={handleAddClusterSubmit} className="add-cluster-form">
<input
type="text"
className="cluster-title-input"
placeholder="Category name..."
value={newClusterName}
onChange={(e) => {
setNewClusterName(e.target.value);
setDuplicateError(false);
}}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Escape') {
setIsAddingCluster(false);
setNewClusterName('');
setDuplicateError(false);
}
}}
/>
{duplicateError && <span className="add-cluster-error">Name already exists</span>}
<div className="add-cluster-actions">
<button type="submit" className="btn-add-confirm">
Create
</button>
<button
type="button"
className="btn-add-cancel"
onClick={() => {
setIsAddingCluster(false);
setNewClusterName('');
setDuplicateError(false);
}}
>
Cancel
</button>
</div>
</form>
) : (
<button className="add-cluster-trigger-btn" onClick={() => setIsAddingCluster(true)}>
<span className="add-cluster-icon">+</span> Add custom category
</button>
)}
</div>
)}

{selectedStrategy && (
<ClusterCard
key={id}
title={selectedStrategy.clusterNames?.[id] || `Cluster ${id + 1}`}
noteIndices={clusters[id]}
title="Uncategorized"
noteIndices={noise}
notes={notes}
tags={selectedStrategy.tags?.[id]}
onRename={(newName) => updateClusterName(id, newName)}
isNoise={true}
onNoteDrop={(noteIndex) => moveNoteToCluster(noteIndex, -1)}
/>
))}
{noise.length > 0 && (
<ClusterCard title="Uncategorized" noteIndices={noise} notes={notes} isNoise={true} />
)}
</div>

Expand Down
Loading
Loading