Merge pull request #123 from resolutionflow/refactor/dashboard-design-critique
refactor: account settings page audit — tokens, a11y, hierarchy
This commit was merged in pull request #123.
This commit is contained in:
@@ -373,7 +373,7 @@ gh run view <id> --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
|
|||||||
|
|
||||||
**105. `npm run build` fails with `EACCES: permission denied` on `dist/` in code-server:** This is a filesystem permission issue in the Docker environment, not a TypeScript error — the TS compilation completes successfully. Use `npx tsc -b` to verify TypeScript cleanly without needing to write to `dist/`.
|
**105. `npm run build` fails with `EACCES: permission denied` on `dist/` in code-server:** This is a filesystem permission issue in the Docker environment, not a TypeScript error — the TS compilation completes successfully. Use `npx tsc -b` to verify TypeScript cleanly without needing to write to `dist/`.
|
||||||
|
|
||||||
---
|
**106. Guard async "select item → load data → apply state" flows with a ref:** When a component lets the user switch between items (chat sessions, flows, scripts) and loads data asynchronously on each switch, the load for item A can complete *after* the user has already switched to item B — overwriting B's state with A's stale data. Fix pattern: keep a `currentSelectionRef = useRef(initialId)` and update it synchronously whenever the selection changes (in every creation/switch path). After every `await`, bail out if `currentSelectionRef.current !== thisItemId`. See `AssistantChatPage.tsx` `selectChat` for the reference implementation (`currentChatRef`).
|
||||||
|
|
||||||
## RBAC & Permissions
|
## RBAC & Permissions
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: import.meta.env.VITE_SENTRY_DSN || "https://23937b8c0cea2484f6a9d5b97d0b7d4b@o4511005918887936.ingest.us.sentry.io/4511005926883328",
|
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||||
environment: import.meta.env.MODE,
|
environment: import.meta.env.MODE,
|
||||||
|
|
||||||
integrations: [
|
integrations: [
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react'
|
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock, Plug, Palette, ShieldCheck } from 'lucide-react'
|
||||||
import { PageMeta } from '@/components/common/PageMeta'
|
import { PageMeta } from '@/components/common/PageMeta'
|
||||||
import { BrandingSettings } from '@/components/settings/BrandingSettings'
|
|
||||||
import { accountsApi } from '@/api/accounts'
|
import { accountsApi } from '@/api/accounts'
|
||||||
import type { Account, AccountMember, AccountInvite } from '@/types'
|
import type { Account, AccountMember, AccountInvite } from '@/types'
|
||||||
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
|
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
|
||||||
@@ -22,7 +21,6 @@ export function AccountSettingsPage() {
|
|||||||
const { isAccountOwner } = usePermissions()
|
const { isAccountOwner } = usePermissions()
|
||||||
const { plan, limits, usage } = useSubscription()
|
const { plan, limits, usage } = useSubscription()
|
||||||
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
|
||||||
const user = useAuthStore((s) => s.user)
|
|
||||||
const subscription = useAuthStore((s) => s.subscription)
|
const subscription = useAuthStore((s) => s.subscription)
|
||||||
|
|
||||||
const [account, setAccount] = useState<Account | null>(null)
|
const [account, setAccount] = useState<Account | null>(null)
|
||||||
@@ -45,8 +43,6 @@ export function AccountSettingsPage() {
|
|||||||
const [inviteEmail, setInviteEmail] = useState('')
|
const [inviteEmail, setInviteEmail] = useState('')
|
||||||
const [inviteRole, setInviteRole] = useState('engineer')
|
const [inviteRole, setInviteRole] = useState('engineer')
|
||||||
const [isInviting, setIsInviting] = useState(false)
|
const [isInviting, setIsInviting] = useState(false)
|
||||||
const [inviteError, setInviteError] = useState<string | null>(null)
|
|
||||||
const [inviteSuccess, setInviteSuccess] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
@@ -86,7 +82,9 @@ export function AccountSettingsPage() {
|
|||||||
const updated = await accountsApi.updateMyAccount({ name: editedName.trim() })
|
const updated = await accountsApi.updateMyAccount({ name: editedName.trim() })
|
||||||
setAccount(updated)
|
setAccount(updated)
|
||||||
setIsEditingName(false)
|
setIsEditingName(false)
|
||||||
|
toast.success('Account name updated')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
toast.error('Failed to update account name')
|
||||||
console.error('Failed to update account name:', err)
|
console.error('Failed to update account name:', err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSavingName(false)
|
setIsSavingName(false)
|
||||||
@@ -98,17 +96,14 @@ export function AccountSettingsPage() {
|
|||||||
if (!inviteEmail.trim()) return
|
if (!inviteEmail.trim()) return
|
||||||
|
|
||||||
setIsInviting(true)
|
setIsInviting(true)
|
||||||
setInviteError(null)
|
|
||||||
setInviteSuccess(null)
|
|
||||||
try {
|
try {
|
||||||
await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole })
|
await accountsApi.createInvite({ email: inviteEmail.trim(), role: inviteRole })
|
||||||
setInviteSuccess(`Invitation sent to ${inviteEmail}`)
|
toast.success(`Invitation sent to ${inviteEmail}`)
|
||||||
setInviteEmail('')
|
setInviteEmail('')
|
||||||
// Refresh invites list
|
|
||||||
const invitesData = await accountsApi.getInvites()
|
const invitesData = await accountsApi.getInvites()
|
||||||
setInvites(invitesData)
|
setInvites(invitesData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setInviteError('Failed to send invitation')
|
toast.error('Failed to send invitation')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsInviting(false)
|
setIsInviting(false)
|
||||||
@@ -135,7 +130,9 @@ export function AccountSettingsPage() {
|
|||||||
try {
|
try {
|
||||||
await accountsApi.removeMember(userId)
|
await accountsApi.removeMember(userId)
|
||||||
setMembers(members.filter((m) => m.id !== userId))
|
setMembers(members.filter((m) => m.id !== userId))
|
||||||
|
toast.success('Member removed')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
toast.error('Failed to remove member')
|
||||||
console.error('Failed to remove member:', err)
|
console.error('Failed to remove member:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +147,7 @@ export function AccountSettingsPage() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
|
<div className="rounded-md border border-danger/20 bg-danger-dim p-4 text-danger">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertCircle className="h-5 w-5" />
|
<AlertCircle className="h-5 w-5" />
|
||||||
{error}
|
{error}
|
||||||
@@ -230,7 +227,7 @@ export function AccountSettingsPage() {
|
|||||||
{isAccountOwner && (
|
{isAccountOwner && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsEditingName(true)}
|
onClick={() => setIsEditingName(true)}
|
||||||
className="text-xs text-foreground hover:underline"
|
className="rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -261,9 +258,9 @@ export function AccountSettingsPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
|
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
|
||||||
plan === 'free' && 'bg-accent text-muted-foreground',
|
plan === 'free' && 'bg-muted text-muted-foreground',
|
||||||
plan === 'pro' && 'bg-accent text-foreground',
|
plan === 'pro' && 'bg-accent-dim text-accent-text',
|
||||||
plan === 'team' && 'bg-accent text-foreground'
|
plan === 'team' && 'bg-accent-dim text-accent-text'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Crown className="h-3.5 w-3.5" />
|
<Crown className="h-3.5 w-3.5" />
|
||||||
@@ -273,11 +270,11 @@ export function AccountSettingsPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
|
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||||
sub.status === 'active' && 'bg-green-500/10 text-emerald-400',
|
sub.status === 'active' && 'bg-success-dim text-success',
|
||||||
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
|
sub.status === 'trialing' && 'bg-info-dim text-info',
|
||||||
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
|
sub.status === 'past_due' && 'bg-warning-dim text-warning',
|
||||||
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
|
sub.status === 'canceled' && 'bg-danger-dim text-danger',
|
||||||
sub.status === 'orphaned' && 'bg-accent text-muted-foreground'
|
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
|
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
|
||||||
@@ -295,7 +292,7 @@ export function AccountSettingsPage() {
|
|||||||
{limits && usage && (
|
{limits && usage && (
|
||||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||||
<UsageStat
|
<UsageStat
|
||||||
label="Trees"
|
label="Flows"
|
||||||
current={usage.tree_count}
|
current={usage.tree_count}
|
||||||
max={limits.max_trees}
|
max={limits.max_trees}
|
||||||
/>
|
/>
|
||||||
@@ -350,12 +347,13 @@ export function AccountSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{member.account_role === 'owner' ? (
|
{member.account_role === 'owner' ? (
|
||||||
<span className="rounded-full px-2.5 py-0.5 text-xs font-medium bg-accent text-foreground">
|
<span className="rounded-full px-2.5 py-0.5 text-xs font-medium bg-accent-dim text-accent-text">
|
||||||
owner
|
owner
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
value={member.account_role}
|
value={member.account_role}
|
||||||
|
aria-label={`Role for ${member.name}`}
|
||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
try {
|
try {
|
||||||
const updated = await accountsApi.updateMemberRole(member.id, e.target.value)
|
const updated = await accountsApi.updateMemberRole(member.id, e.target.value)
|
||||||
@@ -375,15 +373,15 @@ export function AccountSettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
{!member.is_active && (
|
{!member.is_active && (
|
||||||
<span className="rounded-full bg-red-400/10 px-2 py-0.5 text-xs text-red-400">
|
<span className="rounded-full bg-danger-dim px-2 py-0.5 text-xs text-danger">
|
||||||
Inactive
|
Inactive
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{member.account_role !== 'owner' && (
|
{member.account_role !== 'owner' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveMember(member.id)}
|
onClick={() => handleRemoveMember(member.id)}
|
||||||
className="text-muted-foreground hover:text-red-400"
|
className="p-1 text-muted-foreground hover:text-danger"
|
||||||
title="Remove member"
|
aria-label={`Remove ${member.name}`}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -438,12 +436,6 @@ export function AccountSettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{inviteError && (
|
|
||||||
<p className="text-sm text-red-400">{inviteError}</p>
|
|
||||||
)}
|
|
||||||
{inviteSuccess && (
|
|
||||||
<p className="text-sm text-emerald-400">{inviteSuccess}</p>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Pending Invites */}
|
{/* Pending Invites */}
|
||||||
@@ -467,14 +459,14 @@ export function AccountSettingsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs text-muted-foreground">
|
<span className="rounded-full bg-muted px-2.5 py-0.5 text-xs text-muted-foreground">
|
||||||
{invite.role}
|
{invite.role}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleResendInvite(invite.id)}
|
onClick={() => handleResendInvite(invite.id)}
|
||||||
disabled={resendingId === invite.id}
|
disabled={resendingId === invite.id}
|
||||||
className="text-muted-foreground hover:text-foreground disabled:opacity-50"
|
className="p-1 text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||||
title="Resend invite"
|
aria-label={`Resend invite to ${invite.email}`}
|
||||||
>
|
>
|
||||||
{resendingId === invite.id ? (
|
{resendingId === invite.id ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
@@ -494,7 +486,7 @@ export function AccountSettingsPage() {
|
|||||||
{/* Profile Settings Link */}
|
{/* Profile Settings Link */}
|
||||||
<Link
|
<Link
|
||||||
to="/account/profile"
|
to="/account/profile"
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<UserCog className="h-5 w-5 text-muted-foreground" />
|
<UserCog className="h-5 w-5 text-muted-foreground" />
|
||||||
@@ -506,95 +498,89 @@ export function AccountSettingsPage() {
|
|||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Team Categories Link (owners only) */}
|
{/* Team Settings section (owners only) */}
|
||||||
{isAccountOwner && (
|
{isAccountOwner && (
|
||||||
<Link
|
<>
|
||||||
to="/account/categories"
|
<p className="pt-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
Team Settings
|
||||||
>
|
</p>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FolderTree className="h-5 w-5 text-muted-foreground" />
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">Manage tree categories for your team</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Target Lists Link (owners only) */}
|
<Link
|
||||||
{isAccountOwner && (
|
to="/account/categories"
|
||||||
<Link
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
to="/account/target-lists"
|
>
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
<div className="flex items-center gap-3">
|
||||||
>
|
<FolderTree className="h-5 w-5 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-3">
|
<div>
|
||||||
<Server className="h-5 w-5 text-muted-foreground" />
|
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
|
||||||
<div>
|
<p className="text-sm text-muted-foreground">Manage flow categories for your team</p>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Target Lists</h2>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Saved server lists for maintenance flow batch launching</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
</Link>
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chat Retention Link (owners only) */}
|
<Link
|
||||||
{isAccountOwner && (
|
to="/account/target-lists"
|
||||||
<Link
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
to="/account/chat-retention"
|
>
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
<div className="flex items-center gap-3">
|
||||||
>
|
<Server className="h-5 w-5 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-3">
|
<div>
|
||||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
<h2 className="text-lg font-semibold text-foreground">Target Lists</h2>
|
||||||
<div>
|
<p className="text-sm text-muted-foreground">Saved server and device lists for your team</p>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Chat Retention</h2>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Configure AI assistant conversation retention policies</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
</Link>
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Integrations Link (owners only) */}
|
<Link
|
||||||
{isAccountOwner && (
|
to="/account/chat-retention"
|
||||||
<Link
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
to="/account/integrations"
|
>
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
<div className="flex items-center gap-3">
|
||||||
>
|
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-3">
|
<div>
|
||||||
<Plug className="h-5 w-5 text-muted-foreground" />
|
<h2 className="text-lg font-semibold text-foreground">Chat Retention</h2>
|
||||||
<div>
|
<p className="text-sm text-muted-foreground">Configure AI assistant conversation retention policies</p>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Integrations</h2>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Connect your PSA to sync session documentation to tickets</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
</Link>
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Branding Link (owners only) */}
|
<Link
|
||||||
{isAccountOwner && (
|
to="/account/integrations"
|
||||||
<Link
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
to="/account/branding"
|
>
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
<div className="flex items-center gap-3">
|
||||||
>
|
<Plug className="h-5 w-5 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-3">
|
<div>
|
||||||
<Palette className="h-5 w-5 text-muted-foreground" />
|
<h2 className="text-lg font-semibold text-foreground">Integrations</h2>
|
||||||
<div>
|
<p className="text-sm text-muted-foreground">Connect your PSA to sync session documentation to tickets</p>
|
||||||
<h2 className="text-lg font-semibold text-foreground">Branding</h2>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Customize logo, accent color, and company name</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
</Link>
|
||||||
</Link>
|
|
||||||
|
<Link
|
||||||
|
to="/account/branding"
|
||||||
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Palette className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">Branding</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">Customize logo, accent color, and company name</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Feedback Link (all users) */}
|
{/* Feedback Link (all users) */}
|
||||||
<Link
|
<Link
|
||||||
to="/feedback"
|
to="/feedback"
|
||||||
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border-hover transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<MessageSquareText className="h-5 w-5 text-muted-foreground" />
|
<MessageSquareText className="h-5 w-5 text-muted-foreground" />
|
||||||
@@ -606,11 +592,6 @@ export function AccountSettingsPage() {
|
|||||||
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Branding Section (owners only) */}
|
|
||||||
{isAccountOwner && user?.team_id && (
|
|
||||||
<BrandingSettings teamId={user.team_id} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Preferences Section */}
|
{/* Preferences Section */}
|
||||||
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -647,13 +628,13 @@ export function AccountSettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* SSO Section (Task 11) */}
|
{/* SSO Section */}
|
||||||
{isAccountOwner && (
|
{isAccountOwner && (
|
||||||
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<ShieldCheck className="h-5 w-5 text-muted-foreground" />
|
<ShieldCheck className="h-5 w-5 text-muted-foreground" />
|
||||||
<h2 className="text-lg font-semibold text-foreground">Single Sign-On (SSO)</h2>
|
<h2 className="text-lg font-semibold text-foreground">Single Sign-On (SSO)</h2>
|
||||||
<span className="inline-flex items-center rounded-full bg-accent-dim px-2.5 py-0.5 text-xs font-sans text-xs font-medium text-primary">
|
<span className="inline-flex items-center rounded-full bg-accent-dim px-2.5 py-0.5 text-xs font-medium text-primary">
|
||||||
Enterprise
|
Enterprise
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -665,8 +646,8 @@ export function AccountSettingsPage() {
|
|||||||
href="mailto:support@resolutionflow.com?subject=SSO%20Setup%20Request"
|
href="mailto:support@resolutionflow.com?subject=SSO%20Setup%20Request"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
|
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium',
|
||||||
'bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.06)] text-foreground',
|
'bg-muted border border-border text-foreground',
|
||||||
'hover:border-[rgba(255,255,255,0.12)] transition-all'
|
'hover:border-border-hover transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Contact Us
|
Contact Us
|
||||||
@@ -675,9 +656,9 @@ export function AccountSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
<div className="rounded-xl border border-rose-500/20 p-4 sm:p-6">
|
<div className="rounded-xl border border-danger/20 p-4 sm:p-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<AlertTriangle className="h-5 w-5 text-rose-500" />
|
<AlertTriangle className="h-5 w-5 text-danger" />
|
||||||
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
|
<h2 className="text-lg font-semibold text-foreground">Danger Zone</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -693,7 +674,7 @@ export function AccountSettingsPage() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowTransferModal(true)}
|
onClick={() => setShowTransferModal(true)}
|
||||||
className="border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
|
className="border-warning/30 text-warning hover:bg-warning-dim"
|
||||||
>
|
>
|
||||||
Transfer
|
Transfer
|
||||||
</Button>
|
</Button>
|
||||||
@@ -774,7 +755,7 @@ function UsageStat({
|
|||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-1 text-lg font-semibold',
|
'mt-1 text-lg font-semibold',
|
||||||
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-foreground'
|
isAtLimit ? 'text-danger' : isNearLimit ? 'text-warning' : 'text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{current}
|
{current}
|
||||||
@@ -783,11 +764,11 @@ function UsageStat({
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
{!isUnlimited && (
|
{!isUnlimited && (
|
||||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-accent">
|
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full rounded-full transition-all',
|
'h-full rounded-full transition-all',
|
||||||
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
|
isAtLimit ? 'bg-danger' : isNearLimit ? 'bg-warning' : 'bg-primary'
|
||||||
)}
|
)}
|
||||||
style={{ width: `${percentage}%` }}
|
style={{ width: `${percentage}%` }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ export default function AssistantChatPage() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const dragCounterRef = useRef(0)
|
const dragCounterRef = useRef(0)
|
||||||
const prefillHandledRef = useRef(false)
|
const prefillHandledRef = useRef(false)
|
||||||
|
// Tracks the most recently requested active chat ID so in-flight selectChat
|
||||||
|
// calls that complete after the user switches chats don't clobber new state.
|
||||||
|
const currentChatRef = useRef<string | null>(activeChatId)
|
||||||
|
|
||||||
// Persist active chat ID to sessionStorage
|
// Persist active chat ID to sessionStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -214,6 +217,7 @@ export default function AssistantChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectChat = useCallback(async (chatId: string) => {
|
const selectChat = useCallback(async (chatId: string) => {
|
||||||
|
currentChatRef.current = chatId
|
||||||
setActiveChatId(chatId)
|
setActiveChatId(chatId)
|
||||||
// Clear TaskLane when switching chats — will restore from backend if available
|
// Clear TaskLane when switching chats — will restore from backend if available
|
||||||
setShowTaskLane(false)
|
setShowTaskLane(false)
|
||||||
@@ -221,6 +225,10 @@ export default function AssistantChatPage() {
|
|||||||
setActiveActions([])
|
setActiveActions([])
|
||||||
try {
|
try {
|
||||||
const detail = await aiSessionsApi.getSession(chatId)
|
const detail = await aiSessionsApi.getSession(chatId)
|
||||||
|
// Guard: if the user switched to a different chat while this API call was
|
||||||
|
// in flight (e.g. clicked "New Chat"), discard stale results so we don't
|
||||||
|
// clobber the new session's task lane state.
|
||||||
|
if (currentChatRef.current !== chatId) return
|
||||||
setMessages(
|
setMessages(
|
||||||
(detail.conversation_messages || []).map(m => ({
|
(detail.conversation_messages || []).map(m => ({
|
||||||
role: m.role as 'user' | 'assistant',
|
role: m.role as 'user' | 'assistant',
|
||||||
@@ -264,6 +272,7 @@ export default function AssistantChatPage() {
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
currentChatRef.current = session.session_id
|
||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(session.session_id)
|
setActiveChatId(session.session_id)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
@@ -430,6 +439,7 @@ export default function AssistantChatPage() {
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
currentChatRef.current = session.session_id
|
||||||
setChats(prev => [chatItem, ...prev])
|
setChats(prev => [chatItem, ...prev])
|
||||||
setActiveChatId(session.session_id)
|
setActiveChatId(session.session_id)
|
||||||
setMessages([{ role: 'user', content: resumePrompt }])
|
setMessages([{ role: 'user', content: resumePrompt }])
|
||||||
|
|||||||
Reference in New Issue
Block a user