diff --git a/app/ui/src/App.tsx b/app/ui/src/App.tsx
index c4a5db8..c12dbdd 100644
--- a/app/ui/src/App.tsx
+++ b/app/ui/src/App.tsx
@@ -1,19 +1,20 @@
import { IPublicClientApplication } from "@azure/msal-browser";
import { MsalAuthenticationTemplate, MsalProvider } from "@azure/msal-react";
-import { Button, makeStyles } from "@fluentui/react-components";
+import { Avatar, Button, makeStyles } from "@fluentui/react-components";
import { ChevronLeft20Regular } from "@fluentui/react-icons";
import { useLocation, useNavigate } from "react-router-dom";
import logo from './assets/Azure.svg';
import { interactionType, loginRequest } from "./authConfig";
import { NavMenu, Pages } from "./Navigation";
+
type AppProps = {
pca: IPublicClientApplication;
};
const useStyles = makeStyles({
- app: { backgroundColor: '#f5f5f5', color: '#fff', minHeight: '100vh' },
- header: { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', padding: '8px' },
+ app: { backgroundColor: '#f5f5f5', color: '#fff', minHeight: '100vh', overflowX: 'hidden'},
+ header: { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', padding: '8px', width: '100%', boxSizing: 'border-box' },
logo: {
display: 'flex',
alignItems: 'center',
@@ -29,6 +30,11 @@ const useStyles = makeStyles({
},
logoText: { textDecoration: 'none', color: '#000' },
main: { display: 'flex', flexDirection: 'column', alignItems: 'center' },
+ rightSection: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '16px',
+ },
});
function App({ pca }: AppProps) {
@@ -49,7 +55,11 @@ function App({ pca }: AppProps) {
AI Document Review
-
+
+
diff --git a/app/ui/src/Navigation.tsx b/app/ui/src/Navigation.tsx
index 9eb7c5f..349bab3 100644
--- a/app/ui/src/Navigation.tsx
+++ b/app/ui/src/Navigation.tsx
@@ -8,7 +8,6 @@ import {
MenuGroup,
MenuGroupHeader,
MenuDivider,
- Avatar,
MenuItemLink,
} from "@fluentui/react-components";
import {
@@ -19,11 +18,12 @@ import {
DocumentBulletListFilled,
} from "@fluentui/react-icons";
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
-import Agents from "./pages/admin/Agents";
+import AgentManager from "./pages/admin/AgentManager";
import Files from "./pages/files/Files";
import Review from "./pages/review/Review";
-import Settings from "./pages/admin/Settings";
-import useStyles from "./styles/useStyles";
+import SettingManager from "./pages/admin/SettingManager";
+import { DOCUMENTATION_URL } from "./constants";
+import { sharedStyles } from "./styles/sharedStyles";
// paths
@@ -34,12 +34,11 @@ const paths = {
settings: "/admin/settings",
};
-const documentation_url = "https://github.com/Azure-Samples/ai-document-review";
const NavMenu = () => {
const navigate = useNavigate();
const location = useLocation();
- const classes = useStyles()
+ const sharedClasses = sharedStyles();
const isSelected = (path: string) => location.pathname === path;
@@ -54,7 +53,7 @@ const NavMenu = () => {
}
onClick={() => navigate(paths.home)}
- className={isSelected(paths.home) ? classes.selected : ""}
+ className={isSelected(paths.home) ? sharedClasses.selected : ""}
>
Home
@@ -67,27 +66,22 @@ const NavMenu = () => {
}
onClick={() => navigate(paths.adminAgents)}
- className={isSelected(paths.adminAgents) ? classes.selected : ""}
+ className={isSelected(paths.adminAgents) ? sharedClasses.selected : ""}
>
Agents Manager
}
onClick={() => navigate(paths.settings)}
- className={isSelected(paths.settings) ? classes.selected : ""}
+ className={isSelected(paths.settings) ? sharedClasses.selected : ""}
>
Settings
- } href={documentation_url} target="_blank" rel="noopener noreferrer">
+ } href={DOCUMENTATION_URL} target="_blank" rel="noopener noreferrer">
Documentation
-
- {/* Account */}
-
@@ -98,8 +92,8 @@ const Pages = () => (
} />
} />
- } />
- } />
+ } />
+ } />
);
diff --git a/app/ui/src/components/AddCard.tsx b/app/ui/src/components/AddCard.tsx
new file mode 100644
index 0000000..0029617
--- /dev/null
+++ b/app/ui/src/components/AddCard.tsx
@@ -0,0 +1,46 @@
+import { Card, makeStyles, Text, tokens } from '@fluentui/react-components'
+import { AddRegular } from '@fluentui/react-icons'
+
+
+interface AddCardProps {
+ onClick: () => void
+ labelText: string
+}
+
+const componentSyles = makeStyles({
+ addIconText: {
+ textAlign: 'center',
+ fontWeight: tokens.fontWeightSemibold
+ },
+ disabledCard: {
+ width: '200px',
+ maxWidth: '100%',
+ height: '260px'
+ },
+ addIcon: {
+ fontSize: '72px',
+ color: tokens.colorNeutralForeground3,
+ margin: 'auto',
+ opacity: 0.5,
+ pointerEvents: 'none'
+ }
+})
+
+const AddCard: React.FC = ({ onClick, labelText }) => {
+ const componentClasses = componentSyles()
+
+ return (
+ onClick()}
+ >
+
+ {labelText}
+
+ )
+}
+
+export default AddCard
diff --git a/app/ui/src/components/AgentCard.tsx b/app/ui/src/components/AgentCard.tsx
new file mode 100644
index 0000000..8d1690b
--- /dev/null
+++ b/app/ui/src/components/AgentCard.tsx
@@ -0,0 +1,113 @@
+import {
+ Button,
+ Caption1,
+ Card,
+ CardFooter,
+ CardHeader,
+ CardPreview,
+ Text,
+ Tooltip,
+ makeStyles
+} from '@fluentui/react-components'
+import { DeleteRegular, SquareMultipleRegular } from '@fluentui/react-icons'
+import React from 'react'
+import { sharedStyles } from '../styles/sharedStyles'
+import { PromptAgent } from '../types/prompt-agent'
+
+
+const componentSyles = makeStyles({
+ cardHeaderTitle: {
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ maxWidth: '165px',
+ },
+ cardFooter: {
+ marginTop: "auto",
+ display: "flex",
+ justifyContent: "flex-end",
+ gap: "8px"
+ },
+ cardbody: {
+ padding: '12px',
+ width: '175px',
+ textAlign: 'left',
+ display: '-webkit-box',
+ WebkitBoxOrient: 'vertical',
+ WebkitLineClamp: 3,
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-word',
+ maxWidth: '175px',
+ maxHeight: '125px'
+ }
+})
+
+interface AgentCardProps {
+ agent: PromptAgent
+ onViewAgent: (agent: PromptAgent) => void
+ onDeleteAgent: (id: string) => void
+ onEditAgent: (id: string) => void
+ onDuplicateAgent: (id: string) => void
+}
+
+const AgentCard: React.FC = ({
+ agent,
+ onViewAgent,
+ onDeleteAgent,
+ onDuplicateAgent
+}) => {
+ const extractDate = (dateString: string | number | Date | undefined) => {
+ const date = new Date(dateString || '')
+ return !isNaN(date.getTime()) ? date.toLocaleDateString() : 'Date not available'
+ }
+
+ const componentClasses = componentSyles()
+ const sharedClasses = sharedStyles()
+
+ return (
+ onViewAgent(agent)} key={agent.id} className={sharedClasses.card}>
+
+ {agent.name}
+
+ }
+ description={
+
+ {agent.updated_at_UTC
+ ? `Updated: ${extractDate(agent.updated_at_UTC)}`
+ : `Created: ${extractDate(agent.created_at_UTC)}`}
+
+ }
+ />
+
+ {agent.guideline_prompt}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default AgentCard
diff --git a/app/ui/src/components/AgentDialog.tsx b/app/ui/src/components/AgentDialog.tsx
new file mode 100644
index 0000000..7abd0ee
--- /dev/null
+++ b/app/ui/src/components/AgentDialog.tsx
@@ -0,0 +1,308 @@
+import React, { useState, useEffect } from 'react'
+import {
+ Dialog,
+ DialogSurface,
+ DialogBody,
+ DialogTitle,
+ Button,
+ DialogContent,
+ Input,
+ Textarea,
+ Divider,
+ DialogActions,
+ Field,
+ Spinner,
+ Tooltip,
+ makeStyles,
+ tokens
+} from '@fluentui/react-components'
+import { CopyRegular, DismissRegular, SaveRegular } from '@fluentui/react-icons'
+import { addAgent, updateAgent } from '../services/api'
+import ErrorMessage from './ErrorMessage'
+import {
+ PLACEHOLDER_AGENT_GUIDELINE_PROMPT,
+ PLACEHOLDER_AGENT_NAME,
+ PLACEHOLDER_AGENT_TYPE
+} from '../constants'
+import { PromptAgent } from '../types/prompt-agent'
+import { sharedStyles } from '../styles/sharedStyles'
+
+interface AgentDialogProps {
+ agentId: string
+ handleCloseDialog: () => void
+ selectedAgent: PromptAgent
+ showDialog: boolean
+ setShowDialog: (open: boolean) => void
+ mode: 'view' | 'edit' | 'add' | 'duplicate'
+ updateAgentList?: () => void
+}
+
+const componentSyles = makeStyles({
+ largeTextArea: {
+ width: '200%',
+ height: '250px',
+ padding: '8px',
+ marginTop: '2px',
+ marginBottom: '8px'
+ },
+ viewModeText: {
+ border: '1px solid #ccc',
+ borderRadius: '5px',
+ padding: '5px'
+ },
+ largeViewModeText: {
+ width: '170%',
+ whiteSpace: 'pre-wrap',
+ border: '1px solid #ccc',
+ borderRadius: '5px',
+ padding: '5px',
+ marginTop: '2px',
+ marginBottom: '8px',
+ height: '250px',
+ overflowY: 'auto'
+ },
+ bottomLeftButtonContainer: {
+ position: 'absolute',
+ bottom: '16px',
+ right: '16px'
+ },
+ dialogSurface: {
+ paddingBottom: '64px',
+ minHeight: '625px',
+ maxHeight: '90%',
+ minWidth: '50px',
+ maxWidth: '850px',
+ height: '650px',
+ width: '850px',
+ padding: '20px',
+ backgroundColor: tokens.colorNeutralBackground6
+ }
+})
+
+const AgentDialog: React.FC = ({
+ agentId,
+ handleCloseDialog,
+ selectedAgent,
+ showDialog,
+ setShowDialog,
+ mode,
+ updateAgentList
+}) => {
+ const [localAgent, setLocalAgent] = useState(selectedAgent)
+ const [localAgentId, setLocalAgentId] = useState(agentId)
+ const [nameInputError, setNameInputError] = useState('')
+ const [typeInputError, setTypeInputError] = useState('')
+ const [promptInputError, setPromptInputError] = useState('')
+ const [hasError, setHasError] = useState(true)
+ const [hasClientSideError, setHasClientSideError] = useState(false)
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [localMode, setLocalMode] = useState(mode)
+
+ const componentClasses = componentSyles()
+ const sharedClasses = sharedStyles()
+
+ useEffect(() => {
+ setHasError(!!nameInputError || !!typeInputError || !!promptInputError)
+ setHasClientSideError(!!nameInputError || !!typeInputError || !!promptInputError)
+ }, [nameInputError, typeInputError, promptInputError])
+
+ useEffect(() => {
+ if (showDialog) {
+ setLocalAgentId(agentId)
+ setLocalAgent(selectedAgent)
+ }
+ }, [showDialog, selectedAgent, agentId])
+
+ const dialogTitles: { [key: string]: string } = {
+ view: 'View Agent',
+ edit: 'Edit Agent',
+ duplicate: 'Duplicate Agent',
+ add: 'Add Agent'
+ }
+
+ const handleInputChange =
+ (field: 'name' | 'type' | 'guideline_prompt') =>
+ (event: React.ChangeEvent) => {
+ if (localMode === 'view') {
+ setLocalMode ('edit')
+ }
+ const value = event.target.value
+ setLocalAgent((prev) => ({
+ ...prev,
+ [field]: value
+ }))
+
+ const error = hasErrorInInput(value, field === 'guideline_prompt' ? 5000 : 50)
+ if (field === 'name') setNameInputError(error)
+ if (field === 'type') setTypeInputError(error)
+ if (field === 'guideline_prompt') setPromptInputError(error)
+ }
+
+ const hasErrorInInput = (inputValue: string | undefined, limit: number) => {
+ if (!inputValue || !inputValue.trim()) {
+ console.log('inputValue:', inputValue)
+ return 'is required.'
+ }
+
+ if (inputValue.length > limit) {
+ return `cannot exceed ${limit} characters.`
+ }
+
+ const specialCharPattern = /[[],\\.!?:"'-()_$£]/
+
+ if (specialCharPattern.test(inputValue)) {
+ return 'cannot contain special characters.'
+ }
+
+ return ''
+ }
+
+ const callApi = async (
+ id: string,
+ agent: { name: string; guideline_prompt: string; type: string }
+ ) => {
+ if (localMode === 'add' || localMode === 'duplicate') {
+ return addAgent(agent)
+ } else {
+ return updateAgent(id, agent)
+ }
+ }
+
+ const handleSave = async () => {
+ setLoading(true)
+ setNameInputError(hasErrorInInput(localAgent.name, 50))
+ setTypeInputError(hasErrorInInput(localAgent.type, 50))
+ setPromptInputError(hasErrorInInput(localAgent.guideline_prompt, 5000))
+
+ if (
+ !hasClientSideError &&
+ !!localAgent.name &&
+ !!localAgent.type &&
+ !!localAgent.guideline_prompt
+ ) {
+ try {
+ const response = await callApi(localAgentId, localAgent)
+ if (response && updateAgentList) {
+ updateAgentList()
+ handleCloseDialog()
+ setLoading(false)
+ return
+ }
+ setError('Error while saving the agent.')
+ } catch (error) {
+ console.error('Failed to save agent:', error)
+ setError('Failed to save agent.')
+ }
+ } else {
+ setError('Data validaiton failed.')
+ }
+
+ setHasError(true)
+ setLoading(false)
+ }
+
+ const localHandleCloseDialog = () => {
+ if (!loading) {
+ handleCloseDialog()
+ }
+ }
+
+ function onErrorClose(): void {
+ setHasError(false)
+ setLoading(false)
+ setError('')
+ }
+
+ return (
+
+ )
+}
+
+export default AgentDialog
diff --git a/app/ui/src/components/AgentList.tsx b/app/ui/src/components/AgentList.tsx
new file mode 100644
index 0000000..5cd227e
--- /dev/null
+++ b/app/ui/src/components/AgentList.tsx
@@ -0,0 +1,143 @@
+import { useState, useEffect } from 'react'
+import { SkeletonItem } from '@fluentui/react-components'
+import { getAgents, deleteAgent } from '../services/api'
+import AddCard from './AddCard'
+import AgentDialog from './AgentDialog'
+import ErrorMessage from './ErrorMessage'
+import CustomDialog from './CustomDialog'
+import AgentCard from './AgentCard'
+import { PromptAgent } from '../types/prompt-agent'
+import { sharedStyles } from '../styles/sharedStyles'
+
+function AgentList() {
+ const [agents, setAgents] = useState([])
+ const [showDialog, setShowDialog] = useState(false)
+ const [agentInFocus, setAgentInFocus] = useState({
+ id: '',
+ name: '',
+ guideline_prompt: '',
+ type: ''
+ })
+ const [agentLoadError, setAgentLoadError] = useState('')
+ const [mode, setMode] = useState<'view' | 'add' | 'edit' | 'duplicate'>('view')
+ const [agentToBeDeleted, setAgentToBeDeleted] = useState('')
+
+ const sharedClasses = sharedStyles()
+
+ async function fetchAgents() {
+ try {
+ const data = await getAgents()
+ if (data.length === 0) setAgentLoadError('No agents found.')
+ setAgents(data)
+ } catch {
+ setAgentLoadError('There was an issue retrieving agents.')
+ }
+ }
+
+ useEffect(() => {
+ fetchAgents()
+ }, [])
+
+ const updateAgentList = async () => {
+ await fetchAgents()
+ }
+
+ const handleDeleteConfirmation = async (id: string) => {
+ try {
+ await deleteAgent(id)
+ setAgents((prevAgents) => prevAgents.filter((agent) => agent.id !== id))
+ setAgentToBeDeleted('')
+ } catch (error) {
+ console.error('Error deleting agent:', error)
+ }
+ }
+
+ const handleCloseDialog = () => {
+ setAgentInFocus({ id: '', name: '', guideline_prompt: '', type: '' })
+ setShowDialog(false)
+ }
+
+ const handleAddNewAgent = () => {
+ setAgentInFocus({ id: '', name: '', guideline_prompt: '', type: '' })
+ setMode('add')
+ setShowDialog(true)
+ }
+
+ const handleViewAgent = (agent: PromptAgent) => {
+ setAgentInFocus(agent)
+ setMode('view')
+ setShowDialog(true)
+ }
+
+ const handleDeleteAgent = (id: string) => {
+ setAgentToBeDeleted(id)
+ }
+
+ const handleEditAgent = (id: string) => {
+ const agentToEdit = agents.find((a) => a.id === id)
+ if (agentToEdit) setAgentInFocus(agentToEdit)
+ setMode('edit')
+ setShowDialog(true)
+ }
+
+ const handleDuplicateAgent = (id: string) => {
+ const agentToDuplicate = agents.find((a) => a.id === id)
+ if (agentToDuplicate) {
+ const duplicatedAgent = { ...agentToDuplicate, name: `${agentToDuplicate.name} (copy)` }
+ setAgentInFocus(duplicatedAgent)
+ setMode('duplicate')
+ setShowDialog(true)
+ }
+ }
+
+ return (
+
+
+ {agentLoadError && (
+
setAgentLoadError('')}
+ />
+ )}
+
+
+ {agents.length === 0
+ ? Array.from({ length: 2 }, (_, i) =>
)
+ : agents.map((agent) => (
+
+ ))}
+
+
+ {showDialog && (
+
+ )}
+ {agentToBeDeleted && (
+
a.id === agentToBeDeleted)?.name}'`}
+ message="Are you sure you want to delete this agent?"
+ onConfirm={() => handleDeleteConfirmation(agentToBeDeleted)}
+ onCancel={() => setAgentToBeDeleted('')}
+ />
+ )}
+
+ )
+}
+
+export default AgentList
diff --git a/app/ui/src/components/CustomDialog.tsx b/app/ui/src/components/CustomDialog.tsx
new file mode 100644
index 0000000..5e278b6
--- /dev/null
+++ b/app/ui/src/components/CustomDialog.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react'
+import {
+ Dialog,
+ DialogSurface,
+ DialogTitle,
+ DialogBody,
+ DialogActions,
+ Button,
+ Spinner
+} from '@fluentui/react-components'
+
+interface CustomDialogProps {
+ isOpen: boolean
+ title: string
+ message: string
+ onConfirm: () => Promise
+ onCancel: () => void
+}
+
+const CustomDialog: React.FC = ({
+ isOpen,
+ title,
+ message,
+ onConfirm,
+ onCancel
+}) => {
+ const [isLoading, setIsLoading] = useState(false)
+ const [errorMessage, setErrorMessage] = useState(null)
+
+ const handleConfirm = async () => {
+ setIsLoading(true)
+ setErrorMessage(null)
+ try {
+ await onConfirm()
+ } catch (error) {
+ console.error('Error during operation:', error)
+ setErrorMessage('An error occurred. Please try again.')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleCancel = () => {
+ setErrorMessage(null)
+ setIsLoading(false)
+ onCancel()
+ }
+
+ return (
+
+ )
+}
+
+export default CustomDialog
diff --git a/app/ui/src/components/ErrorMessage.tsx b/app/ui/src/components/ErrorMessage.tsx
new file mode 100644
index 0000000..2a14d07
--- /dev/null
+++ b/app/ui/src/components/ErrorMessage.tsx
@@ -0,0 +1,47 @@
+import {
+ Button,
+ makeStyles,
+ MessageBar,
+ MessageBarBody,
+ MessageBarTitle
+} from '@fluentui/react-components'
+import { DismissRegular } from '@fluentui/react-icons'
+
+interface ErrorMessageProps {
+ title: string
+ message: string
+ onClose?: () => void
+}
+
+const componentSyles = makeStyles({
+ messageBar: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ width: '100%'
+ }
+})
+
+const ErrorMessage = ({ title, message, onClose }: ErrorMessageProps) => {
+ const componentClasses = componentSyles()
+ return (
+
+
+
+ {title}
+ {message}
+
+ {onClose && (
+ }
+ onClick={onClose}
+ appearance="subtle"
+ style={{ marginLeft: '1rem' }}
+ />
+ )}
+
+
+ )
+}
+
+export default ErrorMessage
diff --git a/app/ui/src/components/IssueCard.tsx b/app/ui/src/components/IssueCard.tsx
index cfed759..3678e9e 100644
--- a/app/ui/src/components/IssueCard.tsx
+++ b/app/ui/src/components/IssueCard.tsx
@@ -1,22 +1,45 @@
-import { Badge, Button, Caption1Strong, Card, CardFooter, CardHeader, Field, makeStyles, mergeClasses, MessageBar, MessageBarBody, MessageBarTitle, Spinner, Textarea, tokens } from "@fluentui/react-components";
-import { Checkmark16Regular, CheckmarkCircle20Filled, Circle20Filled, Dismiss16Regular, DismissCircle20Filled, PersonFeedback20Filled } from "@fluentui/react-icons";
-import { useState } from "react";
-import { callApi } from "../services/api";
-import { DismissalFeedback, Issue, IssueStatus, ModifiedFields } from "../types/issue";
+import {
+ Badge,
+ Button,
+ Caption1Strong,
+ Card,
+ CardFooter,
+ CardHeader,
+ Field,
+ makeStyles,
+ mergeClasses,
+ MessageBar,
+ MessageBarBody,
+ MessageBarTitle,
+ Spinner,
+ Textarea,
+ tokens
+} from '@fluentui/react-components'
+import {
+ Checkmark16Regular,
+ CheckmarkCircle20Filled,
+ Circle20Filled,
+ Dismiss16Regular,
+ DismissCircle20Filled,
+ PersonFeedback20Filled
+} from '@fluentui/react-icons'
+import { useState } from 'react'
+import { callApi } from '../services/api'
+import { DismissalFeedback, Issue, IssueStatus, ModifiedFields } from '../types/issue'
type IssueCardProps = {
- docId: string;
- issue: Issue;
- selected: boolean;
- onSelect: (issue: Issue) => void;
- onUpdate: (updatedIssue: Issue) => void;
-};
+ docId: string
+ issue: Issue
+ selected: boolean
+ onSelect: (issue: Issue) => void
+ onUpdate: (updatedIssue: Issue) => void
+}
const useStyles = makeStyles({
card: { margin: '5px' },
explanation: { marginTop: '10px' },
accepted: {
- backgroundColor: tokens.colorPaletteGreenBackground1,
+ backgroundColor: tokens.colorPaletteGreenBackground1
},
dismissed: {
backgroundColor: tokens.colorNeutralBackground2
@@ -24,33 +47,33 @@ const useStyles = makeStyles({
header: { height: '40px', textOverflow: 'ellipsis' },
footer: { paddingTop: '10px' },
feedback: {
- backgroundColor: tokens.colorPaletteYellowBackground2,
- },
-});
+ backgroundColor: tokens.colorPaletteYellowBackground2
+ }
+})
export function IssueCard({ docId, issue, selected, onSelect, onUpdate }: IssueCardProps) {
- const classes = useStyles();
+ const classes = useStyles()
function getCardClassName() {
switch (issue.status) {
case IssueStatus.Accepted:
- return mergeClasses(classes.card, classes.accepted);
+ return mergeClasses(classes.card, classes.accepted)
case IssueStatus.Dismissed:
- return mergeClasses(classes.card, classes.dismissed);
+ return mergeClasses(classes.card, classes.dismissed)
default:
- return classes.card;
+ return classes.card
}
}
- const [accepting, setAccepting] = useState(false);
- const [dismissing, setDismissing] = useState(false);
- const [submittingFeedback, setSubmittingFeedback] = useState(false);
- const [addFeedback, setAddFeedback] = useState(false);
- const [modifiedExplanation, setModifiedExplanation] = useState();
- const [modifiedSuggestedFix, setModifiedSuggestedFix] = useState();
- const [feedback, setFeedback] = useState();
- const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
- const [error, setError] = useState();
+ const [accepting, setAccepting] = useState(false)
+ const [dismissing, setDismissing] = useState(false)
+ const [submittingFeedback, setSubmittingFeedback] = useState(false)
+ const [addFeedback, setAddFeedback] = useState(false)
+ const [modifiedExplanation, setModifiedExplanation] = useState()
+ const [modifiedSuggestedFix, setModifiedSuggestedFix] = useState()
+ const [feedback, setFeedback] = useState()
+ const [feedbackSubmitted, setFeedbackSubmitted] = useState(false)
+ const [error, setError] = useState()
/**
* Accepts an issue and posts any modified fields.
@@ -59,14 +82,14 @@ export function IssueCard({ docId, issue, selected, onSelect, onUpdate }: IssueC
// Check if fields have been modified
const modifiedFields: ModifiedFields = {}
if (modifiedExplanation) {
- modifiedFields.explanation = modifiedExplanation;
+ modifiedFields.explanation = modifiedExplanation
}
if (modifiedSuggestedFix) {
- modifiedFields.suggested_fix = modifiedSuggestedFix;
+ modifiedFields.suggested_fix = modifiedSuggestedFix
}
try {
- setAccepting(true);
+ setAccepting(true)
// Send the request
const response = await callApi(
`${docId}/issues/${issue.id}/accept`,
@@ -74,18 +97,18 @@ export function IssueCard({ docId, issue, selected, onSelect, onUpdate }: IssueC
Object.keys(modifiedFields).length ? modifiedFields : undefined
)
// Update issue state
- const updatedIssue = (await response.json()) as Issue;
+ const updatedIssue = (await response.json()) as Issue
if (onUpdate) {
- onUpdate(updatedIssue);
+ onUpdate(updatedIssue)
}
} catch (err) {
if (err instanceof Error) {
- setError(err.message);
+ setError(err.message)
} else {
- setError(String(err));
+ setError(String(err))
}
} finally {
- setAccepting(false);
+ setAccepting(false)
}
}
@@ -94,21 +117,21 @@ export function IssueCard({ docId, issue, selected, onSelect, onUpdate }: IssueC
*/
async function handleDismiss() {
try {
- setDismissing(true);
- const response = await callApi(`${docId}/issues/${issue.id}/dismiss`, 'PATCH');
- const updatedIssue = (await response.json()) as Issue;
+ setDismissing(true)
+ const response = await callApi(`${docId}/issues/${issue.id}/dismiss`, 'PATCH')
+ const updatedIssue = (await response.json()) as Issue
if (onUpdate) {
- onUpdate(updatedIssue);
+ onUpdate(updatedIssue)
}
- setAddFeedback(true);
+ setAddFeedback(true)
} catch (err) {
if (err instanceof Error) {
- setError(err.message);
+ setError(err.message)
} else {
- setError(String(err));
+ setError(String(err))
}
} finally {
- setDismissing(false);
+ setDismissing(false)
}
}
@@ -117,106 +140,120 @@ export function IssueCard({ docId, issue, selected, onSelect, onUpdate }: IssueC
*/
async function handleSubmitFeedback() {
try {
- setSubmittingFeedback(true);
- await callApi(`${docId}/issues/${issue.id}/feedback`, 'PATCH', feedback);
- setFeedbackSubmitted(true);
- setAddFeedback(false);
+ setSubmittingFeedback(true)
+ await callApi(`${docId}/issues/${issue.id}/feedback`, 'PATCH', feedback)
+ setFeedbackSubmitted(true)
+ setAddFeedback(false)
} catch (err) {
if (err instanceof Error) {
- setError(err.message);
+ setError(err.message)
} else {
- setError(String(err));
+ setError(String(err))
}
} finally {
- setSubmittingFeedback(false);
+ setSubmittingFeedback(false)
}
}
return (
- onSelect(issue)} color={tokens.colorNeutralForeground2}>
-
- : issue.status === IssueStatus.Dismissed
- ?
- :
- }
- header={
- { issue.text }
- }
- />
+ onSelect(issue)}
+ color={tokens.colorNeutralForeground2}
+ >
+
+ ) : issue.status === IssueStatus.Dismissed ? (
+
+ ) : (
+
+ )
+ }
+ header={
+
+ {issue.text}
+
+ }
+ />
-
- { issue.type }
-
+
+ {issue.type}
+
- {
- selected && <>
+ {selected && (
+ <>
- {
- error &&
+ {error && (
+
Error
- { error }
+ {error}
- }
- {
- issue.status === IssueStatus.NotReviewed &&
+ )}
+ {issue.status === IssueStatus.NotReviewed && (
+
- ) :
- }
+ icon={accepting ? : }
onClick={handleAccept}
>
Accept
- ) :
- }
+ icon={dismissing ? : }
onClick={handleDismiss}
>
Dismiss
- }
- {
- addFeedback &&
- } header="Help us improve" />
+ )}
+ {addFeedback && (
+
+
+ }
+ header="Help us improve"
+ />
- }
- {
- feedbackSubmitted &&
+ )}
+ {feedbackSubmitted && (
+
Feedback submitted
Thanks for helping improve AI Document Review!
- }
+ )}
>
- }
+ )}
)
}
diff --git a/app/ui/src/components/PageHeader.tsx b/app/ui/src/components/PageHeader.tsx
index 296d035..e51c5ad 100644
--- a/app/ui/src/components/PageHeader.tsx
+++ b/app/ui/src/components/PageHeader.tsx
@@ -1,29 +1,45 @@
-import React from "react";
-import aiDocIcon from '../assets/ai-doc.png';
-import useStyles from "../styles/useStyles";
+import React from 'react'
+import aiDocIcon from '../assets/ai-doc.png'
+import { sharedStyles } from '../styles/sharedStyles'
+import { makeStyles } from '@fluentui/react-components'
interface PageHeaderProps {
- title: string;
- description: string;
+ title: string
+ description: string
+ customElement?: React.ReactNode
}
-const PageHeader: React.FC = ({ title, description }) => {
- const classes = useStyles();
+const componentStyles = makeStyles({
+ headerContent: {
+ margin: '16px',
+ color: '#000',
+ maxWidth: '800px',
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+})
+
+const PageHeader: React.FC = ({ title, description, customElement }) => {
+ const sharedClasses = sharedStyles()
+ const componentClasses = componentStyles()
return (
-
-
-
+
+
+
{title}
{description}
+ {customElement &&
{customElement}
}
- );
-};
+ )
+}
-export default PageHeader;
+export default PageHeader
diff --git a/app/ui/src/constants.ts b/app/ui/src/constants.ts
new file mode 100644
index 0000000..91ee5cb
--- /dev/null
+++ b/app/ui/src/constants.ts
@@ -0,0 +1,7 @@
+export const DOCUMENTATION_URL = "https://github.com/Azure-Samples/ai-document-review";
+export const PLACEHOLDER_AGENT_NAME = "e.g. Version 1";
+export const PLACEHOLDER_AGENT_TYPE = "e.g. Legal Review";
+export const PLACEHOLDER_AGENT_GUIDELINE_PROMPT = `e.g.
+
+You are an AI agent that reviews documents from a Legal perspective.
+Please review the document and provide feedback...`;
diff --git a/app/ui/src/pages/admin/AgentManager.tsx b/app/ui/src/pages/admin/AgentManager.tsx
new file mode 100644
index 0000000..a8d0377
--- /dev/null
+++ b/app/ui/src/pages/admin/AgentManager.tsx
@@ -0,0 +1,28 @@
+import { Divider } from '@fluentui/react-components';
+import PageHeader from '../../components/PageHeader';
+import AgentList from '../../components/AgentList';
+import { sharedStyles } from '../../styles/sharedStyles';
+
+
+function AgentManager() {
+ const sharedClasses = sharedStyles();
+
+ return (
+
+ {/* Header Section */}
+
+
+
+ {/* Agents List and Add Agent */}
+
+
+ );
+}
+
+export default AgentManager;
diff --git a/app/ui/src/pages/admin/Agents.tsx b/app/ui/src/pages/admin/Agents.tsx
deleted file mode 100644
index d9b7b9b..0000000
--- a/app/ui/src/pages/admin/Agents.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Divider } from '@fluentui/react-components'
-import PageHeader from '../../components/PageHeader'
-import useStyles from '../../styles/useStyles'
-
-function Agents() {
- const classes = useStyles()
-
- return (
-
- )
-}
-
-export default Agents
diff --git a/app/ui/src/pages/admin/Settings.tsx b/app/ui/src/pages/admin/SettingManager.tsx
similarity index 68%
rename from app/ui/src/pages/admin/Settings.tsx
rename to app/ui/src/pages/admin/SettingManager.tsx
index 8750603..224389c 100644
--- a/app/ui/src/pages/admin/Settings.tsx
+++ b/app/ui/src/pages/admin/SettingManager.tsx
@@ -1,18 +1,19 @@
import { Divider } from '@fluentui/react-components'
import PageHeader from '../../components/PageHeader'
-import useStyles from '../../styles/useStyles'
+import { sharedStyles } from '../../styles/sharedStyles'
+
function Settings() {
- const classes = useStyles()
+ const sharedClasses = sharedStyles()
return (
-
+
)
}
diff --git a/app/ui/src/pages/files/Files.tsx b/app/ui/src/pages/files/Files.tsx
index 0343fa9..288fd7f 100644
--- a/app/ui/src/pages/files/Files.tsx
+++ b/app/ui/src/pages/files/Files.tsx
@@ -6,6 +6,8 @@ import { useNavigate } from 'react-router-dom';
import pdfIcon from '../../assets/pdf.svg';
import { listBlobs, uploadBlob } from '../../services/storage';
import PageHeader from '../../components/PageHeader';
+import { DOCUMENTATION_URL } from '../../constants';
+import ErrorMessage from '../../components/ErrorMessage';
const flex = {
gap: "16px",
@@ -106,16 +108,13 @@ function Files() {
+ customElement={
}
+ />
{
- blobError &&
-
- Error loading files
- { blobError }
-
-
+ blobError &&
+
}
{
- checkError &&
-
- Error running check
- { checkError }
-
-
+ checkError &&
+
}
{
docId &&