File size: 6,544 Bytes
3786a3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a67b1ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3786a3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a67b1ec
 
 
 
 
3786a3f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
'use client';

import { useEffect, useState } from 'react';
import { Network, FileText, AlertCircle } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { useGraphData } from '@/hooks/useGraphData';
import { GraphVisualization } from '@/components/graph/GraphVisualization';
import { useGraphStore } from '@/store/graph';
import api from '@/lib/axios';

interface Community {
  id: number;
  label: string;
  entityCount: number;
  relationshipCount: number;
  summary: string;
  keyEntities: string[];
}

export function CommunityView() {
  const { error: graphError } = useGraphData();
  const [communities, setCommunities] = useState<Community[]>([]);
  const [docSummary, setDocSummary] = useState('');
  const [loading, setLoading] = useState(true);
  const [fetchError, setFetchError] = useState<string | null>(null);
  const [tab, setTab] = useState('cards');
  const selectEntity = useGraphStore((s) => s.selectEntity);
  const setHighlighted = useGraphStore((s) => s.setHighlighted);

  useEffect(() => {
    let pollTimer: ReturnType<typeof setTimeout>;

    function fetchCommunities() {
      setLoading(true);
      api
        .get('/graph/communities/')
        .then(({ data }) => {
          const isGenerating = data.loading === true;

          if (isGenerating) {
            // Backend is generating in background — poll again in 15s
            setLoading(true);
            pollTimer = setTimeout(fetchCommunities, 15000);
            return;
          }

          const c = data.communities || data.results || data;
          if (Array.isArray(c) && c.length) {
            setCommunities(
              c.map((x: any) => ({
                id: x.id,
                label: x.label || `Community ${x.id}`,
                entityCount: x.member_count ?? x.entity_count ?? x.entityCount ?? 0,
                relationshipCount: x.relationship_count ?? x.relationshipCount ?? 0,
                summary: x.summary || '',
                keyEntities: x.members ?? x.key_entities ?? x.keyEntities ?? [],
              }))
            );
            setDocSummary(data.document_summary || data.summary || '');
          }
          setFetchError(null);
          setLoading(false);
        })
        .catch(() => {
          setFetchError('Failed to load communities. Backend may be unavailable.');
          setLoading(false);
        });
    }

    fetchCommunities();

    return () => {
      if (pollTimer) clearTimeout(pollTimer);
    };
  }, []);

  function expand(id: number) {
    const c = communities.find((x) => x.id === id);
    if (c && c.keyEntities.length) {
      setHighlighted(c.keyEntities, []);
      setTab('graph');
    }
  }

  return (
    <Tabs value={tab} onValueChange={setTab} className="flex h-full flex-col p-4">
      <div className="mb-3 flex flex-wrap items-center justify-between gap-3">
        <TabsList>
          <TabsTrigger value="cards">Cards</TabsTrigger>
          <TabsTrigger value="graph">Graph</TabsTrigger>
        </TabsList>
      </div>

      {graphError && (
        <div className="mb-3 flex items-start gap-2 rounded-md border border-warning/30 bg-warning/5 p-3 text-sm text-warning">
          <AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
          <span>{graphError}</span>
        </div>
      )}

      {fetchError && (
        <div className="mb-3 flex items-start gap-2 rounded-md border border-error/30 bg-error/5 p-3 text-sm text-error">
          <AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
          <span>{fetchError}</span>
        </div>
      )}

      {docSummary && (
        <Card className="mb-4">
          <CardContent className="flex items-start gap-2 p-4 text-sm text-text-secondary">
            <FileText className="mt-0.5 h-4 w-4 shrink-0 text-accent-cyan" />
            <span>{docSummary}</span>
          </CardContent>
        </Card>
      )}

      <TabsContent value="cards" className="mt-0 flex-1 overflow-y-auto scrollbar-thin">
        {loading ? (
          <div className="flex flex-col items-center justify-center gap-3 py-12">
            <div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-violet border-t-transparent" />
            <p className="text-sm text-text-muted">Generating communities from your knowledge graph…</p>
            <p className="text-xs text-text-muted">This may take up to 30 seconds. Page will auto-refresh.</p>
          </div>
        ) : communities.length === 0 ? (
          <div className="flex items-center justify-center py-12 text-sm text-text-muted">No communities found. Upload documents to generate communities.</div>
        ) : (
        <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
          {communities.map((c) => (
            <Card key={c.id} className="flex flex-col">
              <CardContent className="flex flex-1 flex-col gap-3 p-4">
                <div className="flex items-center gap-2">
                  <Network className="h-4 w-4 text-accent-violet" />
                  <h3 className="font-semibold">{c.label}</h3>
                </div>
                <div className="flex gap-3 text-xs text-text-muted">
                  <span>{c.entityCount} entities</span>
                  <span>{c.relationshipCount} relationships</span>
                </div>
                <p className="flex-1 text-sm text-text-secondary">{c.summary}</p>
                <div className="flex flex-wrap gap-1">
                  {c.keyEntities.map((e) => (
                    <span
                      key={e}
                      className="rounded bg-bg-elevated px-2 py-0.5 text-xs text-text-secondary"
                    >
                      {e}
                    </span>
                  ))}
                </div>
                <button
                  onClick={() => expand(c.id)}
                  className="self-start rounded-md border border-accent-violet/40 px-3 py-1.5 text-xs font-medium text-accent-violet hover:bg-accent-violet/10"
                >
                  Expand Graph ▶
                </button>
              </CardContent>
            </Card>
          ))}
        </div>
        )}
      </TabsContent>

      <TabsContent value="graph" className="mt-0 min-h-0 flex-1">
        <div className="relative h-full rounded-md border border-border bg-bg-base">
          <GraphVisualization height="100%" />
        </div>
      </TabsContent>
    </Tabs>
  );
}