mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-15 04:21:38 +00:00
Make memory graph cluster colors configurable
This commit is contained in:
parent
4675904159
commit
0b076297cc
5 changed files with 63 additions and 26 deletions
|
|
@ -81,13 +81,16 @@ function LineIcon({
|
|||
)
|
||||
}
|
||||
|
||||
function ClusterSwatches() {
|
||||
const swatches = ["#58C7E8", "#E7BC52", "#74D680", "#D47B75", "#A789E8"]
|
||||
function ClusterSwatches({ colors }: { colors: GraphThemeColors }) {
|
||||
const swatches =
|
||||
colors.clusterColors.length > 0
|
||||
? colors.clusterColors.slice(0, 5)
|
||||
: [colors.memStrokeDefault]
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 2, flexShrink: 0 }}>
|
||||
{swatches.map((color) => (
|
||||
{swatches.map((color, index) => (
|
||||
<span
|
||||
key={color}
|
||||
key={`${color}-${index}`}
|
||||
style={{
|
||||
width: 7,
|
||||
height: 12,
|
||||
|
|
@ -563,7 +566,7 @@ export const Legend = memo(function Legend({
|
|||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<span style={sectionLabelStyle}>Color</span>
|
||||
<div style={statusRowStyle}>
|
||||
<ClusterSwatches />
|
||||
<ClusterSwatches colors={colors} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,18 @@
|
|||
import type { GraphThemeColors } from "./types"
|
||||
|
||||
export const DEFAULT_CLUSTER_COLORS = [
|
||||
"#58C7E8",
|
||||
"#E7BC52",
|
||||
"#74D680",
|
||||
"#D47B75",
|
||||
"#A789E8",
|
||||
"#62C5A8",
|
||||
"#74ABD8",
|
||||
"#C78AC8",
|
||||
"#D18A58",
|
||||
"#8BCB6F",
|
||||
]
|
||||
|
||||
export const MEMORY_BORDER_KEYS = {
|
||||
forgotten: "memBorderForgotten",
|
||||
expiring: "memBorderExpiring",
|
||||
|
|
@ -66,4 +79,5 @@ export const DEFAULT_COLORS: GraphThemeColors = {
|
|||
popoverTextMuted: "#94a3b8",
|
||||
controlBg: "#1a1f29",
|
||||
controlBorder: "#2A2F36",
|
||||
clusterColors: DEFAULT_CLUSTER_COLORS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
GraphThemeColors,
|
||||
MemoryNodeData,
|
||||
} from "../types"
|
||||
import { DEFAULT_CLUSTER_COLORS } from "../constants"
|
||||
import { hashString } from "../utils/hash"
|
||||
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
|
@ -21,19 +22,6 @@ const APPEND_CANDIDATES_PER_RING = 18
|
|||
const APPEND_MAX_RINGS = 8
|
||||
const APPEND_SPATIAL_CELL_SIZE = APPEND_CLUSTER_RADIUS + APPEND_AREA_GAP + 120
|
||||
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5))
|
||||
const CLUSTER_COLORS = [
|
||||
"#58C7E8",
|
||||
"#E7BC52",
|
||||
"#74D680",
|
||||
"#D47B75",
|
||||
"#A789E8",
|
||||
"#62C5A8",
|
||||
"#74ABD8",
|
||||
"#C78AC8",
|
||||
"#D18A58",
|
||||
"#8BCB6F",
|
||||
]
|
||||
|
||||
export interface ClusterAssignment {
|
||||
key: string
|
||||
color: string
|
||||
|
|
@ -107,12 +95,18 @@ function getMemoryRingCapacity(ring: number): number {
|
|||
return Math.max(8, Math.floor((2 * Math.PI * radius) / MEMORY_ORBIT_SPACING))
|
||||
}
|
||||
|
||||
export function getClusterColor(key: string): string {
|
||||
return CLUSTER_COLORS[hashString(key) % CLUSTER_COLORS.length] as string
|
||||
export function getClusterColor(
|
||||
key: string,
|
||||
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
|
||||
): string {
|
||||
const palette =
|
||||
clusterColors.length > 0 ? clusterColors : DEFAULT_CLUSTER_COLORS
|
||||
return palette[hashString(key) % palette.length] as string
|
||||
}
|
||||
|
||||
export function computeClusterAssignments(
|
||||
documents: GraphApiDocument[],
|
||||
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
|
||||
): Map<string, ClusterAssignment> {
|
||||
const adjacency = new Map<string, Set<string>>()
|
||||
const docByMemory = new Map<string, string>()
|
||||
|
|
@ -180,7 +174,7 @@ export function computeClusterAssignments(
|
|||
: `relation:${firstDocId}:${firstId}`
|
||||
const assignment = {
|
||||
key,
|
||||
color: getClusterColor(key),
|
||||
color: getClusterColor(key, clusterColors),
|
||||
size: component.length,
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +199,7 @@ function getMemoryRelationTargets(mem: GraphApiMemory): Record<string, string> {
|
|||
function getDocumentClusterAssignment(
|
||||
doc: GraphApiDocument,
|
||||
assignments: Map<string, ClusterAssignment>,
|
||||
clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
|
||||
): ClusterAssignment {
|
||||
const counts = new Map<
|
||||
string,
|
||||
|
|
@ -229,7 +224,7 @@ function getDocumentClusterAssignment(
|
|||
return (
|
||||
best?.assignment ?? {
|
||||
key: `doc:${doc.id}`,
|
||||
color: getClusterColor(`doc:${doc.id}`),
|
||||
color: getClusterColor(`doc:${doc.id}`, clusterColors),
|
||||
size: 1,
|
||||
}
|
||||
)
|
||||
|
|
@ -492,7 +487,10 @@ export function useGraphData(
|
|||
? buildAppendSpatialGrid(appendPlacementNodes)
|
||||
: null
|
||||
let appendIndex = 0
|
||||
const clusterAssignments = computeClusterAssignments(documents)
|
||||
const clusterAssignments = computeClusterAssignments(
|
||||
documents,
|
||||
colors.clusterColors,
|
||||
)
|
||||
|
||||
const result: GraphNode[] = []
|
||||
// Spiral layout: documents form a compact spiral core, memories orbit
|
||||
|
|
@ -509,7 +507,11 @@ export function useGraphData(
|
|||
|
||||
for (let docIdx = 0; docIdx < docCount; docIdx++) {
|
||||
const doc = documents[docIdx]
|
||||
const docCluster = getDocumentClusterAssignment(doc, clusterAssignments)
|
||||
const docCluster = getDocumentClusterAssignment(
|
||||
doc,
|
||||
clusterAssignments,
|
||||
colors.clusterColors,
|
||||
)
|
||||
const angle = docIdx * goldenAngle
|
||||
const radius = spiralScale * Math.sqrt((docIdx + 1) / docCount)
|
||||
const initialX = cx + Math.cos(angle) * radius
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ function readCssVar(name: string, fallback: string): string {
|
|||
return val || fallback
|
||||
}
|
||||
|
||||
function readCssColorList(name: string, fallback: string[]): string[] {
|
||||
if (typeof document === "undefined") return fallback
|
||||
const val = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(name)
|
||||
.trim()
|
||||
if (!val) return fallback
|
||||
const colors = val
|
||||
.split(",")
|
||||
.map((color) => color.trim())
|
||||
.filter(Boolean)
|
||||
return colors.length > 0 ? colors : fallback
|
||||
}
|
||||
|
||||
function resolveColors(): GraphThemeColors {
|
||||
return {
|
||||
bg: readCssVar("--graph-bg", DEFAULT_COLORS.bg),
|
||||
|
|
@ -71,6 +84,10 @@ function resolveColors(): GraphThemeColors {
|
|||
"--graph-control-border",
|
||||
DEFAULT_COLORS.controlBorder,
|
||||
),
|
||||
clusterColors: readCssColorList(
|
||||
"--graph-cluster-colors",
|
||||
DEFAULT_COLORS.clusterColors,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,13 +124,13 @@ export function useGraphTheme(
|
|||
const overrideKey = overrides
|
||||
? Object.entries(overrides)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([k, v]) => `${k}:${v}`)
|
||||
.map(([k, v]) => `${k}:${Array.isArray(v) ? v.join("|") : v}`)
|
||||
.join(",")
|
||||
: ""
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
|
||||
const merged = useMemo(
|
||||
() => (overrides ? { ...colors, ...overrides } : colors),
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
|
||||
[colors, overrideKey],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export interface GraphThemeColors {
|
|||
popoverTextMuted: string
|
||||
controlBg: string
|
||||
controlBorder: string
|
||||
clusterColors: string[]
|
||||
}
|
||||
|
||||
export interface GraphCanvasProps {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue