shak3008 commited on
Commit
a46398e
·
1 Parent(s): 0d686d3

feat: complete authentication system with dw login/logout/whoami and Web Studio auth modal

Browse files
diffweave/cli/commands/auth_cmd.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ `dw login`, `dw logout`, and `dw whoami` commands.
3
+
4
+ Manages authentication with DocWeave via JWT tokens, OAuth, demo logins, or API keys.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import Optional
10
+ import typer
11
+ import httpx
12
+
13
+ from diffweave.cli.config import get_active_workspace
14
+ from diffweave.cli.credentials import save_credentials, load_credentials, clear_credentials
15
+ from diffweave.cli.output import print_success, print_error, print_info, print_warning
16
+
17
+
18
+ def login_command(
19
+ email: Optional[str] = typer.Option(None, "--email", "-e", help="DocWeave user email"),
20
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="DocWeave user password"),
21
+ token: Optional[str] = typer.Option(None, "--token", "-t", help="Personal Access Token or JWT"),
22
+ demo: bool = typer.Option(False, "--demo", help="Log in instantly using demo evaluator account"),
23
+ url: Optional[str] = typer.Option(None, "--url", help="DocWeave Server URL (default: http://localhost:8000 or DOCWEAVE_MCP_URL)"),
24
+ ):
25
+ """
26
+ Authenticate with DocWeave and securely store credentials.
27
+ """
28
+ server_url = url or os.environ.get("DOCWEAVE_MCP_URL") or "http://localhost:8000"
29
+ server_url = server_url.rstrip("/")
30
+
31
+ # 1. Direct Token Login
32
+ if token:
33
+ save_credentials(access_token=token, email="token-user", username="API Token User", mcp_url=server_url)
34
+ print_success("Successfully authenticated via API token!")
35
+ print_info(f" Stored in: ~/.diffweave/credentials.json")
36
+ return
37
+
38
+ # 2. Demo Login
39
+ if demo:
40
+ print_info(f"Authenticating with DocWeave demo evaluator account at [cyan]{server_url}[/cyan]...")
41
+ try:
42
+ with httpx.Client(timeout=15.0) as client:
43
+ resp = client.post(f"{server_url}/auth/demo-login")
44
+ if resp.status_code == 200:
45
+ data = resp.json()
46
+ tok = data.get("access_token")
47
+ user_email = data.get("email", "demo@docweave.io")
48
+ username = data.get("username", "Demo Evaluator")
49
+ save_credentials(access_token=tok, email=user_email, username=username, mcp_url=server_url)
50
+ print_success(f"Logged in as [bold white]{username}[/bold white] ({user_email})!")
51
+ print_info(" Credentials saved to ~/.diffweave/credentials.json")
52
+ return
53
+ else:
54
+ print_error(f"Demo login failed: {resp.text}")
55
+ raise typer.Exit(code=1)
56
+ except Exception as e:
57
+ # If server not running on localhost, fallback to local demo session
58
+ print_warning(f"Could not reach remote server at {server_url}: {e}")
59
+ print_info("Creating local demo evaluator session...")
60
+ save_credentials(
61
+ access_token="demo-offline-evaluator-token",
62
+ email="demo@docweave.io",
63
+ username="Demo Evaluator (Offline)",
64
+ mcp_url=server_url,
65
+ )
66
+ print_success("Logged in as [bold white]Demo Evaluator (Offline)[/bold white]!")
67
+ return
68
+
69
+ # 3. Interactive Email & Password Prompt
70
+ if not email:
71
+ email = typer.prompt("DocWeave Email")
72
+ if not password:
73
+ password = typer.prompt("DocWeave Password", hide_input=True)
74
+
75
+ print_info(f"Authenticating [bold white]{email}[/bold white] against [cyan]{server_url}[/cyan]...")
76
+
77
+ try:
78
+ with httpx.Client(timeout=15.0) as client:
79
+ resp = client.post(
80
+ f"{server_url}/auth/login",
81
+ data={"username": email, "password": password},
82
+ )
83
+ if resp.status_code == 200:
84
+ data = resp.json()
85
+ tok = data.get("access_token")
86
+ save_credentials(access_token=tok, email=email, username=email.split("@")[0], mcp_url=server_url)
87
+ print_success(f"Successfully authenticated as [bold white]{email}[/bold white]!")
88
+ print_info(" Session token stored in ~/.diffweave/credentials.json")
89
+ else:
90
+ print_error(f"Login failed: {resp.text}")
91
+ raise typer.Exit(code=1)
92
+ except httpx.ConnectError:
93
+ print_error(f"Could not connect to DocWeave server at {server_url}.")
94
+ print_info("Ensure the server is running, or pass --token / --url to specify a remote host.")
95
+ raise typer.Exit(code=1)
96
+ except Exception as e:
97
+ print_error(f"Authentication error: {e}")
98
+ raise typer.Exit(code=1)
99
+
100
+
101
+ def logout_command():
102
+ """
103
+ Log out and remove stored DocWeave credentials.
104
+ """
105
+ cleared = clear_credentials()
106
+ if cleared:
107
+ print_success("Logged out successfully. Stored credentials removed.")
108
+ else:
109
+ print_info("No stored credentials found.")
110
+
111
+
112
+ def whoami_command():
113
+ """
114
+ Display current authenticated identity and active workspace.
115
+ """
116
+ creds = load_credentials()
117
+ if not creds:
118
+ print_warning("Not currently logged in. Run 'dw login' or 'dw login --demo' to authenticate.")
119
+ else:
120
+ print_success("Authenticated Session Active")
121
+ print_info(f" User: [bold white]{creds.get('username', 'N/A')}[/bold white]")
122
+ print_info(f" Email: [cyan]{creds.get('email', 'N/A')}[/cyan]")
123
+ token_preview = creds.get('access_token', '')[:16] + "..." if creds.get('access_token') else "N/A"
124
+ print_info(f" Token: [dim]{token_preview}[/dim]")
125
+ if creds.get('mcp_url'):
126
+ print_info(f" Server: [cyan]{creds.get('mcp_url')}[/cyan]")
127
+
128
+ try:
129
+ active_ws = get_active_workspace()
130
+ print_info(f" Workspace: [bold green]{active_ws}[/bold green]")
131
+ except Exception:
132
+ print_info(" Workspace: [yellow]None (Run 'dw init' to bind one)[/yellow]")
diffweave/cli/credentials.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DiffWeave Credentials Store.
3
+
4
+ Manages persistent user authentication tokens, API keys, and sessions.
5
+ Stores credentials in ~/.diffweave/credentials.json or local .diffweave/credentials.json.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+
15
+ def get_credentials_dir() -> Path:
16
+ """Returns the user-level ~/.diffweave directory, creating it if needed."""
17
+ creds_dir = Path.home() / ".diffweave"
18
+ creds_dir.mkdir(parents=True, exist_ok=True)
19
+ return creds_dir
20
+
21
+
22
+ def get_credentials_file() -> Path:
23
+ return get_credentials_dir() / "credentials.json"
24
+
25
+
26
+ def save_credentials(
27
+ access_token: str,
28
+ email: Optional[str] = None,
29
+ username: Optional[str] = None,
30
+ mcp_url: Optional[str] = None,
31
+ ) -> None:
32
+ data = {
33
+ "access_token": access_token,
34
+ "email": email or "",
35
+ "username": username or "",
36
+ "mcp_url": mcp_url or "",
37
+ }
38
+ creds_file = get_credentials_file()
39
+ with open(creds_file, "w", encoding="utf-8") as f:
40
+ json.dump(data, f, indent=2)
41
+
42
+
43
+ def load_credentials() -> Optional[dict[str, Any]]:
44
+ # 1. Environment variable override
45
+ env_token = os.environ.get("DOCWEAVE_API_KEY") or os.environ.get("DIFFWEAVE_TOKEN")
46
+ if env_token:
47
+ return {
48
+ "access_token": env_token,
49
+ "email": os.environ.get("DOCWEAVE_USER_EMAIL", "env-user"),
50
+ "username": os.environ.get("DOCWEAVE_USERNAME", "Environment User"),
51
+ "mcp_url": os.environ.get("DOCWEAVE_MCP_URL", ""),
52
+ }
53
+
54
+ # 2. Local workspace override
55
+ local_creds = Path(".diffweave/credentials.json")
56
+ if local_creds.exists():
57
+ try:
58
+ with open(local_creds, "r", encoding="utf-8") as f:
59
+ return json.load(f)
60
+ except Exception:
61
+ pass
62
+
63
+ # 3. User global credentials
64
+ creds_file = get_credentials_file()
65
+ if creds_file.exists():
66
+ try:
67
+ with open(creds_file, "r", encoding="utf-8") as f:
68
+ return json.load(f)
69
+ except Exception:
70
+ pass
71
+
72
+ return None
73
+
74
+
75
+ def clear_credentials() -> bool:
76
+ cleared = False
77
+ creds_file = get_credentials_file()
78
+ if creds_file.exists():
79
+ try:
80
+ creds_file.unlink()
81
+ cleared = True
82
+ except Exception:
83
+ pass
84
+
85
+ local_creds = Path(".diffweave/credentials.json")
86
+ if local_creds.exists():
87
+ try:
88
+ local_creds.unlink()
89
+ cleared = True
90
+ except Exception:
91
+ pass
92
+
93
+ return cleared
94
+
95
+
96
+ def get_access_token() -> Optional[str]:
97
+ creds = load_credentials()
98
+ if creds:
99
+ return creds.get("access_token")
100
+ return None
diffweave/cli/main.py CHANGED
@@ -20,6 +20,7 @@ from diffweave.cli.commands.log_cmd import log_command
20
  from diffweave.cli.commands.search_cmd import search_command
21
  from diffweave.cli.commands.doctor_cmd import doctor_command
22
  from diffweave.cli.commands.rules_cmd import rules_app
 
23
 
24
  console = Console()
25
 
@@ -32,6 +33,9 @@ app = typer.Typer(
32
 
33
  # Register top-level commands
34
  app.command(name="init", help="Initialize a DiffWeave workspace (.diffweave/)")(init_command)
 
 
 
35
  app.command(name="add", help="Stage and ingest documents into workspace")(add_command)
36
  app.command(name="status", help="Show workspace state, document staging, and pending reviews")(status_command)
37
  app.command(name="proposals", help="List and inspect extracted knowledge proposals (PRs)")(proposals_command)
 
20
  from diffweave.cli.commands.search_cmd import search_command
21
  from diffweave.cli.commands.doctor_cmd import doctor_command
22
  from diffweave.cli.commands.rules_cmd import rules_app
23
+ from diffweave.cli.commands.auth_cmd import login_command, logout_command, whoami_command
24
 
25
  console = Console()
26
 
 
33
 
34
  # Register top-level commands
35
  app.command(name="init", help="Initialize a DiffWeave workspace (.diffweave/)")(init_command)
36
+ app.command(name="login", help="Authenticate with DocWeave via email/password, demo, or API token")(login_command)
37
+ app.command(name="logout", help="Log out and remove stored credentials")(logout_command)
38
+ app.command(name="whoami", help="Display current authenticated user identity and workspace")(whoami_command)
39
  app.command(name="add", help="Stage and ingest documents into workspace")(add_command)
40
  app.command(name="status", help="Show workspace state, document staging, and pending reviews")(status_command)
41
  app.command(name="proposals", help="List and inspect extracted knowledge proposals (PRs)")(proposals_command)
diffweave/mcp/client.py CHANGED
@@ -3,8 +3,7 @@ DiffWeave MCP Client Adapter.
3
 
4
  Provides a unified, resilient interface to DocWeave's MCP services.
5
  Supports:
6
- 1. Remote HTTP/SSE MCP transport (via DOCWEAVE_MCP_URL + DOCWEAVE_API_KEY)
7
- -> Use this when DiffWeave is deployed to the web!
8
  2. In-process dispatch (for high-speed local development with DocWeave on laptop).
9
  3. Embedded standalone engine (offline / fallback mode).
10
  """
@@ -36,6 +35,18 @@ class DiffWeaveMCPClient:
36
  self.backend_dir = None
37
  self._standalone_engine = None
38
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  if self.mcp_url:
40
  # Production remote cloud mode (e.g., https://your-docweave.hf.space)
41
  self.mode = "remote"
 
3
 
4
  Provides a unified, resilient interface to DocWeave's MCP services.
5
  Supports:
6
+ 1. Remote HTTP/SSE MCP transport (via DOCWEAVE_MCP_URL + DOCWEAVE_API_KEY or ~/.diffweave/credentials.json).
 
7
  2. In-process dispatch (for high-speed local development with DocWeave on laptop).
8
  3. Embedded standalone engine (offline / fallback mode).
9
  """
 
35
  self.backend_dir = None
36
  self._standalone_engine = None
37
 
38
+ # Auto-resolve from ~/.diffweave/credentials.json if available
39
+ try:
40
+ from diffweave.cli.credentials import load_credentials
41
+ creds = load_credentials()
42
+ if creds:
43
+ if not self.api_key and creds.get("access_token"):
44
+ self.api_key = creds.get("access_token")
45
+ if not self.mcp_url and creds.get("mcp_url"):
46
+ self.mcp_url = creds.get("mcp_url")
47
+ except Exception:
48
+ pass
49
+
50
  if self.mcp_url:
51
  # Production remote cloud mode (e.g., https://your-docweave.hf.space)
52
  self.mode = "remote"
studio/src/api/client.js CHANGED
@@ -1,30 +1,94 @@
1
  /**
2
  * DiffWeave Studio API Client
3
  * Talks directly to the FastAPI bridge, which delegates to DocWeave MCP Server.
 
4
  */
5
 
6
  const API_BASE = '/api';
7
 
 
 
 
 
 
 
 
 
 
8
  export const api = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  async getHealth() {
10
- const res = await fetch(`${API_BASE}/health`);
11
  return res.json();
12
  },
13
 
14
  async getMCPTools() {
15
- const res = await fetch(`${API_BASE}/mcp/tools`);
16
  return res.json();
17
  },
18
 
 
 
 
19
  async getWorkspaces() {
20
- const res = await fetch(`${API_BASE}/workspaces`);
21
  return res.json();
22
  },
23
 
24
  async createWorkspace(name, description = '') {
25
  const res = await fetch(`${API_BASE}/workspaces`, {
26
  method: 'POST',
27
- headers: { 'Content-Type': 'application/json' },
28
  body: JSON.stringify({ name, description }),
29
  });
30
  if (!res.ok) throw new Error(await res.text());
@@ -32,106 +96,135 @@ export const api = {
32
  },
33
 
34
  async getWorkspaceStatus(workspaceId) {
35
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/status`);
36
  return res.json();
37
  },
38
 
 
 
 
39
  async getDocuments(workspaceId) {
40
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/documents`);
41
  return res.json();
42
  },
43
 
44
  async uploadDocument(workspaceId, file) {
45
  const formData = new FormData();
46
  formData.append('file', file);
47
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/upload`, {
 
 
 
 
48
  method: 'POST',
 
49
  body: formData,
50
  });
51
  if (!res.ok) throw new Error(await res.text());
52
  return res.json();
53
  },
54
 
55
- async getProposals(workspaceId) {
56
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/proposals`);
 
 
 
 
 
 
57
  return res.json();
58
  },
59
 
60
- async reviewProposal(workspaceId, proposalId, decision, comments = null) {
61
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/proposals/${proposalId}/review`, {
62
  method: 'POST',
63
- headers: { 'Content-Type': 'application/json' },
64
  body: JSON.stringify({ decision, comments }),
65
  });
66
  if (!res.ok) throw new Error(await res.text());
67
  return res.json();
68
  },
69
 
70
- async batchReviewProposals(workspaceId, decision, proposalIds = null, comments = null) {
71
  const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/proposals/batch-review`, {
72
  method: 'POST',
73
- headers: { 'Content-Type': 'application/json' },
74
  body: JSON.stringify({ decision, proposal_ids: proposalIds, comments }),
75
  });
76
  if (!res.ok) throw new Error(await res.text());
77
  return res.json();
78
  },
79
 
 
 
 
80
  async getSemanticDiff(workspaceId) {
81
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/diff`);
82
  return res.json();
83
  },
84
 
85
- async getValidation(workspaceId) {
86
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/validate`);
 
 
 
87
  return res.json();
88
  },
89
 
90
- async getKnowledgeGraph(workspaceId) {
91
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/graph`);
 
 
 
 
 
92
  return res.json();
93
  },
94
 
95
- async getKnowledge(workspaceId, type = null, status = null) {
96
- const params = new URLSearchParams();
97
- if (type) params.append('type', type);
98
- if (status) params.append('status', status);
99
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/knowledge?${params.toString()}`);
100
  return res.json();
101
  },
102
 
103
- async searchKnowledge(workspaceId, query) {
104
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/search?q=${encodeURIComponent(query)}`);
105
  return res.json();
106
  },
107
 
108
- async getRules(workspaceId) {
109
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/rules`);
 
 
 
110
  return res.json();
111
  },
112
 
113
- async createRule(workspaceId, name, operator, configuration) {
114
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/rules`, {
115
- method: 'POST',
116
- headers: { 'Content-Type': 'application/json' },
117
- body: JSON.stringify({ name, operator, configuration }),
118
- });
119
- if (!res.ok) throw new Error(await res.text());
120
  return res.json();
121
  },
122
 
123
- async enableRule(ruleId) {
124
- const res = await fetch(`${API_BASE}/rules/${ruleId}/enable`, { method: 'POST' });
125
  return res.json();
126
  },
127
 
128
- async disableRule(ruleId) {
129
- const res = await fetch(`${API_BASE}/rules/${ruleId}/disable`, { method: 'POST' });
 
 
 
130
  return res.json();
131
  },
132
 
133
- async getActivity(workspaceId, limit = 50) {
134
- const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/activity?limit=${limit}`);
135
  return res.json();
136
  },
137
  };
 
1
  /**
2
  * DiffWeave Studio API Client
3
  * Talks directly to the FastAPI bridge, which delegates to DocWeave MCP Server.
4
+ * Supports Bearer token authentication and user sessions.
5
  */
6
 
7
  const API_BASE = '/api';
8
 
9
+ function getHeaders(custom = {}) {
10
+ const token = localStorage.getItem('diffweave_token');
11
+ const headers = { 'Content-Type': 'application/json', ...custom };
12
+ if (token) {
13
+ headers['Authorization'] = `Bearer ${token}`;
14
+ }
15
+ return headers;
16
+ }
17
+
18
  export const api = {
19
+ // -------------------------------------------------------------------------
20
+ // Authentication & Session
21
+ // -------------------------------------------------------------------------
22
+ async login(username, password) {
23
+ const res = await fetch(`${API_BASE}/auth/login`, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ username, password }),
27
+ });
28
+ if (!res.ok) throw new Error(await res.text());
29
+ const data = await res.json();
30
+ if (data.access_token) {
31
+ localStorage.setItem('diffweave_token', data.access_token);
32
+ localStorage.setItem('diffweave_user', JSON.stringify(data));
33
+ }
34
+ return data;
35
+ },
36
+
37
+ async demoLogin() {
38
+ const res = await fetch(`${API_BASE}/auth/demo-login`, { method: 'POST' });
39
+ if (!res.ok) throw new Error(await res.text());
40
+ const data = await res.json();
41
+ if (data.access_token) {
42
+ localStorage.setItem('diffweave_token', data.access_token);
43
+ localStorage.setItem('diffweave_user', JSON.stringify(data));
44
+ }
45
+ return data;
46
+ },
47
+
48
+ setApiKey(token) {
49
+ localStorage.setItem('diffweave_token', token);
50
+ localStorage.setItem('diffweave_user', JSON.stringify({ email: 'api-key-user', username: 'API Key User' }));
51
+ },
52
+
53
+ logout() {
54
+ localStorage.removeItem('diffweave_token');
55
+ localStorage.removeItem('diffweave_user');
56
+ },
57
+
58
+ getCurrentUser() {
59
+ try {
60
+ const u = localStorage.getItem('diffweave_user');
61
+ return u ? JSON.parse(u) : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ },
66
+
67
+ // -------------------------------------------------------------------------
68
+ // Health & MCP
69
+ // -------------------------------------------------------------------------
70
  async getHealth() {
71
+ const res = await fetch(`${API_BASE}/health`, { headers: getHeaders() });
72
  return res.json();
73
  },
74
 
75
  async getMCPTools() {
76
+ const res = await fetch(`${API_BASE}/mcp/tools`, { headers: getHeaders() });
77
  return res.json();
78
  },
79
 
80
+ // -------------------------------------------------------------------------
81
+ // Workspaces
82
+ // -------------------------------------------------------------------------
83
  async getWorkspaces() {
84
+ const res = await fetch(`${API_BASE}/workspaces`, { headers: getHeaders() });
85
  return res.json();
86
  },
87
 
88
  async createWorkspace(name, description = '') {
89
  const res = await fetch(`${API_BASE}/workspaces`, {
90
  method: 'POST',
91
+ headers: getHeaders(),
92
  body: JSON.stringify({ name, description }),
93
  });
94
  if (!res.ok) throw new Error(await res.text());
 
96
  },
97
 
98
  async getWorkspaceStatus(workspaceId) {
99
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/status`, { headers: getHeaders() });
100
  return res.json();
101
  },
102
 
103
+ // -------------------------------------------------------------------------
104
+ // Documents
105
+ // -------------------------------------------------------------------------
106
  async getDocuments(workspaceId) {
107
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/documents`, { headers: getHeaders() });
108
  return res.json();
109
  },
110
 
111
  async uploadDocument(workspaceId, file) {
112
  const formData = new FormData();
113
  formData.append('file', file);
114
+ const token = localStorage.getItem('diffweave_token');
115
+ const headers = {};
116
+ if (token) headers['Authorization'] = `Bearer ${token}`;
117
+
118
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/documents/upload`, {
119
  method: 'POST',
120
+ headers,
121
  body: formData,
122
  });
123
  if (!res.ok) throw new Error(await res.text());
124
  return res.json();
125
  },
126
 
127
+ // -------------------------------------------------------------------------
128
+ // Proposals & PRs
129
+ // -------------------------------------------------------------------------
130
+ async getProposals(workspaceId, status) {
131
+ const url = status
132
+ ? `${API_BASE}/workspaces/${workspaceId}/proposals?status=${status}`
133
+ : `${API_BASE}/workspaces/${workspaceId}/proposals`;
134
+ const res = await fetch(url, { headers: getHeaders() });
135
  return res.json();
136
  },
137
 
138
+ async reviewProposal(proposalId, decision, comments = '') {
139
+ const res = await fetch(`${API_BASE}/proposals/${proposalId}/review`, {
140
  method: 'POST',
141
+ headers: getHeaders(),
142
  body: JSON.stringify({ decision, comments }),
143
  });
144
  if (!res.ok) throw new Error(await res.text());
145
  return res.json();
146
  },
147
 
148
+ async batchReview(workspaceId, decision, proposalIds = null, comments = '') {
149
  const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/proposals/batch-review`, {
150
  method: 'POST',
151
+ headers: getHeaders(),
152
  body: JSON.stringify({ decision, proposal_ids: proposalIds, comments }),
153
  });
154
  if (!res.ok) throw new Error(await res.text());
155
  return res.json();
156
  },
157
 
158
+ // -------------------------------------------------------------------------
159
+ // Semantic Diff
160
+ // -------------------------------------------------------------------------
161
  async getSemanticDiff(workspaceId) {
162
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/diff`, { headers: getHeaders() });
163
  return res.json();
164
  },
165
 
166
+ // -------------------------------------------------------------------------
167
+ // Policy Rules
168
+ // -------------------------------------------------------------------------
169
+ async getRules(workspaceId) {
170
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/rules`, { headers: getHeaders() });
171
  return res.json();
172
  },
173
 
174
+ async createRule(workspaceId, name, operator, configuration) {
175
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/rules`, {
176
+ method: 'POST',
177
+ headers: getHeaders(),
178
+ body: JSON.stringify({ name, operator, configuration }),
179
+ });
180
+ if (!res.ok) throw new Error(await res.text());
181
  return res.json();
182
  },
183
 
184
+ async deleteRule(ruleId) {
185
+ const res = await fetch(`${API_BASE}/rules/${ruleId}`, {
186
+ method: 'DELETE',
187
+ headers: getHeaders(),
188
+ });
189
  return res.json();
190
  },
191
 
192
+ async validateProposals(workspaceId) {
193
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/validate`, { headers: getHeaders() });
194
  return res.json();
195
  },
196
 
197
+ // -------------------------------------------------------------------------
198
+ // Knowledge Register & Graph
199
+ // -------------------------------------------------------------------------
200
+ async getKnowledgeItems(workspaceId) {
201
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/knowledge`, { headers: getHeaders() });
202
  return res.json();
203
  },
204
 
205
+ async searchKnowledge(workspaceId, query) {
206
+ const res = await fetch(
207
+ `${API_BASE}/workspaces/${workspaceId}/knowledge/search?q=${encodeURIComponent(query)}`,
208
+ { headers: getHeaders() }
209
+ );
 
 
210
  return res.json();
211
  },
212
 
213
+ async getKnowledgeGraph(workspaceId) {
214
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/graph`, { headers: getHeaders() });
215
  return res.json();
216
  },
217
 
218
+ // -------------------------------------------------------------------------
219
+ // Audit Trail & Metrics
220
+ // -------------------------------------------------------------------------
221
+ async getActivityFeed(workspaceId) {
222
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/activity`, { headers: getHeaders() });
223
  return res.json();
224
  },
225
 
226
+ async getWorkflows(workspaceId) {
227
+ const res = await fetch(`${API_BASE}/workspaces/${workspaceId}/workflows`, { headers: getHeaders() });
228
  return res.json();
229
  },
230
  };
studio/src/components/Navbar.jsx CHANGED
@@ -1,4 +1,5 @@
1
- import React, { useState } from 'react';
 
2
 
3
  export default function Navbar({
4
  workspaces,
@@ -13,6 +14,20 @@ export default function Navbar({
13
  const [newWsName, setNewWsName] = useState('');
14
  const [newWsDesc, setNewWsDesc] = useState('');
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  const handleCreate = async (e) => {
17
  e.preventDefault();
18
  if (!newWsName.trim()) return;
@@ -22,6 +37,51 @@ export default function Navbar({
22
  setShowNewModal(false);
23
  };
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  const pendingCount = stats?.pending_proposals || 0;
26
 
27
  return (
@@ -31,7 +91,7 @@ export default function Navbar({
31
  {/* Logo & Workspace Selector */}
32
  <div className="flex items-center space-x-6">
33
  <div className="flex items-center space-x-2">
34
- <span className="text-xl text-emerald-400 font-bold tracking-tight">◈ DiffWeave</span>
35
  <span className="text-xs px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono">
36
  Studio
37
  </span>
@@ -94,16 +154,155 @@ export default function Navbar({
94
  })}
95
  </nav>
96
 
97
- {/* MCP Status Indicator */}
98
  <div className="flex items-center space-x-3">
 
99
  <div className="flex items-center space-x-2 px-2.5 py-1 rounded-full bg-emerald-950/40 border border-emerald-500/30 text-emerald-400 text-xs font-mono">
100
  <span className="w-2 h-2 rounded-full bg-emerald-400 pulse-glow"></span>
101
  <span>MCP: 29 Tools</span>
102
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
  </div>
105
  </div>
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  {/* New Workspace Modal */}
108
  {showNewModal && (
109
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
@@ -118,30 +317,30 @@ export default function Navbar({
118
  placeholder="e.g. Clinical Research Protocols"
119
  value={newWsName}
120
  onChange={(e) => setNewWsName(e.target.value)}
121
- className="w-full bg-[#162032] border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
122
  />
123
  </div>
124
  <div className="mb-6">
125
- <label className="block text-xs font-medium text-slate-400 mb-1">Description (Optional)</label>
126
  <textarea
127
- rows="2"
128
- placeholder="Workspace purpose or project notes"
129
  value={newWsDesc}
130
  onChange={(e) => setNewWsDesc(e.target.value)}
131
- className="w-full bg-[#162032] border border-slate-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-emerald-500"
132
- />
 
133
  </div>
134
  <div className="flex justify-end space-x-3">
135
  <button
136
  type="button"
137
  onClick={() => setShowNewModal(false)}
138
- className="px-4 py-2 text-xs font-medium text-slate-400 hover:text-white rounded-lg bg-slate-800"
139
  >
140
  Cancel
141
  </button>
142
  <button
143
  type="submit"
144
- className="px-4 py-2 text-xs font-medium text-black bg-emerald-400 hover:bg-emerald-300 rounded-lg font-semibold"
145
  >
146
  Create Workspace
147
  </button>
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { api } from '../api/client';
3
 
4
  export default function Navbar({
5
  workspaces,
 
14
  const [newWsName, setNewWsName] = useState('');
15
  const [newWsDesc, setNewWsDesc] = useState('');
16
 
17
+ // Auth Modal State
18
+ const [showAuthModal, setShowAuthModal] = useState(false);
19
+ const [currentUser, setCurrentUser] = useState(null);
20
+ const [authMode, setAuthMode] = useState('demo'); // 'demo' | 'login' | 'apikey'
21
+ const [loginEmail, setLoginEmail] = useState('');
22
+ const [loginPassword, setLoginPassword] = useState('');
23
+ const [customApiKey, setCustomApiKey] = useState('');
24
+ const [authLoading, setAuthLoading] = useState(false);
25
+ const [authError, setAuthError] = useState('');
26
+
27
+ useEffect(() => {
28
+ setCurrentUser(api.getCurrentUser());
29
+ }, []);
30
+
31
  const handleCreate = async (e) => {
32
  e.preventDefault();
33
  if (!newWsName.trim()) return;
 
37
  setShowNewModal(false);
38
  };
39
 
40
+ const handleDemoLogin = async () => {
41
+ setAuthLoading(true);
42
+ setAuthError('');
43
+ try {
44
+ const user = await api.demoLogin();
45
+ setCurrentUser(user);
46
+ setShowAuthModal(false);
47
+ } catch (err) {
48
+ // Fallback local demo user
49
+ api.setApiKey('demo-evaluator-token');
50
+ setCurrentUser({ username: 'Demo Evaluator', email: 'demo@docweave.io' });
51
+ setShowAuthModal(false);
52
+ } finally {
53
+ setAuthLoading(false);
54
+ }
55
+ };
56
+
57
+ const handleEmailLogin = async (e) => {
58
+ e.preventDefault();
59
+ setAuthLoading(true);
60
+ setAuthError('');
61
+ try {
62
+ const user = await api.login(loginEmail, loginPassword);
63
+ setCurrentUser(user);
64
+ setShowAuthModal(false);
65
+ } catch (err) {
66
+ setAuthError('Authentication failed: ' + (err.message || 'Invalid credentials'));
67
+ } finally {
68
+ setAuthLoading(false);
69
+ }
70
+ };
71
+
72
+ const handleSaveApiKey = (e) => {
73
+ e.preventDefault();
74
+ if (!customApiKey.trim()) return;
75
+ api.setApiKey(customApiKey.trim());
76
+ setCurrentUser({ username: 'API Key User', email: 'pat@docweave.io' });
77
+ setShowAuthModal(false);
78
+ };
79
+
80
+ const handleLogout = () => {
81
+ api.logout();
82
+ setCurrentUser(null);
83
+ };
84
+
85
  const pendingCount = stats?.pending_proposals || 0;
86
 
87
  return (
 
91
  {/* Logo & Workspace Selector */}
92
  <div className="flex items-center space-x-6">
93
  <div className="flex items-center space-x-2">
94
+ <span className="text-xl text-emerald-400 font-bold tracking-tight">🧶 DiffWeave</span>
95
  <span className="text-xs px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono">
96
  Studio
97
  </span>
 
154
  })}
155
  </nav>
156
 
157
+ {/* Right Status Controls */}
158
  <div className="flex items-center space-x-3">
159
+ {/* MCP Status Indicator */}
160
  <div className="flex items-center space-x-2 px-2.5 py-1 rounded-full bg-emerald-950/40 border border-emerald-500/30 text-emerald-400 text-xs font-mono">
161
  <span className="w-2 h-2 rounded-full bg-emerald-400 pulse-glow"></span>
162
  <span>MCP: 29 Tools</span>
163
  </div>
164
+
165
+ {/* Auth / User Control */}
166
+ {currentUser ? (
167
+ <div className="flex items-center space-x-2 bg-slate-900 border border-slate-700 px-2.5 py-1 rounded-lg text-xs">
168
+ <div className="w-5 h-5 rounded-full bg-emerald-600 text-white flex items-center justify-center font-bold text-[10px]">
169
+ {currentUser.username ? currentUser.username[0].toUpperCase() : 'U'}
170
+ </div>
171
+ <span className="text-slate-200 font-medium max-w-[100px] truncate">{currentUser.username}</span>
172
+ <button
173
+ onClick={handleLogout}
174
+ className="text-slate-500 hover:text-red-400 text-xs ml-1"
175
+ title="Log out"
176
+ >
177
+ ✕
178
+ </button>
179
+ </div>
180
+ ) : (
181
+ <button
182
+ onClick={() => setShowAuthModal(true)}
183
+ className="px-3 py-1 text-xs font-medium rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition shadow-sm"
184
+ >
185
+ Sign In / API Key
186
+ </button>
187
+ )}
188
  </div>
189
  </div>
190
  </div>
191
 
192
+ {/* Auth / API Key Modal */}
193
+ {showAuthModal && (
194
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
195
+ <div className="bg-[#101726] border border-slate-700 p-6 rounded-xl w-full max-w-md shadow-2xl">
196
+ <div className="flex justify-between items-center mb-4">
197
+ <h3 className="text-lg font-semibold text-white">Connect DocWeave Account</h3>
198
+ <button onClick={() => setShowAuthModal(false)} className="text-slate-400 hover:text-white">✕</button>
199
+ </div>
200
+
201
+ {/* Tabs */}
202
+ <div className="flex border-b border-slate-800 mb-4 text-xs font-medium">
203
+ <button
204
+ onClick={() => setAuthMode('demo')}
205
+ className={`pb-2 px-3 ${authMode === 'demo' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
206
+ >
207
+ ⚡ 1-Click Demo
208
+ </button>
209
+ <button
210
+ onClick={() => setAuthMode('login')}
211
+ className={`pb-2 px-3 ${authMode === 'login' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
212
+ >
213
+ Email Login
214
+ </button>
215
+ <button
216
+ onClick={() => setAuthMode('apikey')}
217
+ className={`pb-2 px-3 ${authMode === 'apikey' ? 'border-b-2 border-emerald-400 text-emerald-400' : 'text-slate-400'}`}
218
+ >
219
+ API Key / PAT
220
+ </button>
221
+ </div>
222
+
223
+ {authError && (
224
+ <div className="p-2 mb-3 rounded bg-red-950/50 border border-red-500/30 text-red-300 text-xs">
225
+ {authError}
226
+ </div>
227
+ )}
228
+
229
+ {authMode === 'demo' && (
230
+ <div>
231
+ <p className="text-xs text-slate-300 mb-4 leading-relaxed">
232
+ Log in immediately as an evaluator demo user. No registration or email verification required.
233
+ </p>
234
+ <button
235
+ onClick={handleDemoLogin}
236
+ disabled={authLoading}
237
+ className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition"
238
+ >
239
+ {authLoading ? 'Authenticating...' : 'Enter as Demo Evaluator'}
240
+ </button>
241
+ </div>
242
+ )}
243
+
244
+ {authMode === 'login' && (
245
+ <form onSubmit={handleEmailLogin} className="space-y-3">
246
+ <div>
247
+ <label className="block text-xs font-medium text-slate-400 mb-1">Email</label>
248
+ <input
249
+ type="email"
250
+ required
251
+ placeholder="user@docweave.io"
252
+ value={loginEmail}
253
+ onChange={(e) => setLoginEmail(e.target.value)}
254
+ className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
255
+ />
256
+ </div>
257
+ <div>
258
+ <label className="block text-xs font-medium text-slate-400 mb-1">Password</label>
259
+ <input
260
+ type="password"
261
+ required
262
+ placeholder="••••••••"
263
+ value={loginPassword}
264
+ onChange={(e) => setLoginPassword(e.target.value)}
265
+ className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
266
+ />
267
+ </div>
268
+ <button
269
+ type="submit"
270
+ disabled={authLoading}
271
+ className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition mt-2"
272
+ >
273
+ {authLoading ? 'Signing In...' : 'Sign In'}
274
+ </button>
275
+ </form>
276
+ )}
277
+
278
+ {authMode === 'apikey' && (
279
+ <form onSubmit={handleSaveApiKey} className="space-y-3">
280
+ <div>
281
+ <label className="block text-xs font-medium text-slate-400 mb-1">Personal Access Token (PAT) / JWT</label>
282
+ <input
283
+ type="text"
284
+ required
285
+ placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6Ik..."
286
+ value={customApiKey}
287
+ onChange={(e) => setCustomApiKey(e.target.value)}
288
+ className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
289
+ />
290
+ </div>
291
+ <p className="text-[11px] text-slate-400 leading-normal">
292
+ Paste a JWT bearer token or API key generated from DocWeave to authenticate all requests from this Studio.
293
+ </p>
294
+ <button
295
+ type="submit"
296
+ className="w-full py-2 px-4 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-sm transition mt-2"
297
+ >
298
+ Save & Connect API Key
299
+ </button>
300
+ </form>
301
+ )}
302
+ </div>
303
+ </div>
304
+ )}
305
+
306
  {/* New Workspace Modal */}
307
  {showNewModal && (
308
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
 
317
  placeholder="e.g. Clinical Research Protocols"
318
  value={newWsName}
319
  onChange={(e) => setNewWsName(e.target.value)}
320
+ className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
321
  />
322
  </div>
323
  <div className="mb-6">
324
+ <label className="block text-xs font-medium text-slate-400 mb-1">Description (optional)</label>
325
  <textarea
326
+ placeholder="Clinical documentation & knowledge validation..."
 
327
  value={newWsDesc}
328
  onChange={(e) => setNewWsDesc(e.target.value)}
329
+ rows="3"
330
+ className="w-full bg-[#161F32] border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:ring-1 focus:ring-emerald-500"
331
+ ></textarea>
332
  </div>
333
  <div className="flex justify-end space-x-3">
334
  <button
335
  type="button"
336
  onClick={() => setShowNewModal(false)}
337
+ className="px-4 py-2 text-sm text-slate-400 hover:text-white transition"
338
  >
339
  Cancel
340
  </button>
341
  <button
342
  type="submit"
343
+ className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg transition"
344
  >
345
  Create Workspace
346
  </button>