Skip to content

Commit

Permalink
AI App Ramp up + Presence Integration
Browse files Browse the repository at this point in the history
  • Loading branch information
chentong7 committed Oct 18, 2024
1 parent a390d2f commit 178e9e3
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 2 deletions.
4 changes: 4 additions & 0 deletions examples/apps/ai-collab/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ OPEN_AI_KEY=<your-openai-key>
NEXT_PUBLIC_SPE_CLIENT_ID=<client-id-of-the-Entra-App-for-SPE>
NEXT_PUBLIC_SPE_ENTRA_TENANT_ID=<id-of-the-Entra-tenant-where-the-app-registration-lives>
NEXT_PUBLIC_SPE_CONTAINER_TYPE_ID=<container-type-Id-in-SPE>


AZURE_CLIENT_ID=<client-id-of-the-App>
AZURE_TENANT_ID=<tenant-id-of-the-App>
26 changes: 25 additions & 1 deletion examples/apps/ai-collab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,30 @@
"start": "next dev",
"start:server": "tinylicious"
},
"dependencies": {},
"c8": {
"all": true,
"cache-dir": "nyc/.cache",
"exclude": [
"src/test/**/*.*ts",
"dist/test/**/*.*js"
],
"exclude-after-remap": false,
"include": [
"src/**/*.*ts",
"dist/**/*.*js"
],
"report-dir": "nyc/report",
"reporter": [
"cobertura",
"html",
"text"
],
"temp-directory": "nyc/.nyc_output"
},
"dependencies": {
"@fluid-experimental/presence": "workspace:~",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@azure/identity": "^4.4.1",
"@azure/msal-browser": "^3.25.0",
Expand Down Expand Up @@ -63,6 +86,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rimraf": "^4.4.0",
"source-map-loader": "^5.0.0",
"tinylicious": "^5.0.0",
"typechat": "^0.1.1",
"typescript": "~5.4.5",
Expand Down
11 changes: 11 additions & 0 deletions examples/apps/ai-collab/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

"use client";

import { acquirePresenceViaDataObject } from "@fluid-experimental/presence";
import type { IFluidContainer } from "@fluidframework/fluid-static";
import { type TreeView } from "@fluidframework/tree";
import {
Expand All @@ -19,7 +20,10 @@ import {
} from "@mui/material";
import React, { useEffect, useState } from "react";

import { buildUserPresence, type UserPresence } from "./tasks-list/presence";

import { TaskGroup } from "@/components/TaskGroup";
import UserProfilePhoto from "@/components/UserProfilePhoto";
import {
CONTAINER_SCHEMA,
INITIAL_APP_STATE,
Expand Down Expand Up @@ -48,6 +52,7 @@ export async function createAndInitializeContainer(): Promise<
export default function TasksListPage(): JSX.Element {
const [selectedTaskGroup, setSelectedTaskGroup] = useState<SharedTreeTaskGroup>();
const [treeView, setTreeView] = useState<TreeView<typeof SharedTreeAppState>>();
const [onlineUsers, setOnlineUsers] = useState<UserPresence>();

const { container, isFluidInitialized, data } = useFluidContainerNextJs(
containerIdFromUrl(),
Expand All @@ -58,6 +63,11 @@ export default function TasksListPage(): JSX.Element {
(fluidContainer) => {
const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION);
setTreeView(_treeView);

const presence = acquirePresenceViaDataObject(fluidContainer.initialObjects.presence);
const _onlineUsers = buildUserPresence(presence);
setOnlineUsers(_onlineUsers);

return { sharedTree: _treeView };
},
);
Expand All @@ -80,6 +90,7 @@ export default function TasksListPage(): JSX.Element {
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
maxWidth={"lg"}
>
{onlineUsers && <UserProfilePhoto onlineUsers={onlineUsers} />}
<Typography variant="h2" sx={{ my: 3 }}>
My Work Items
</Typography>
Expand Down
200 changes: 200 additions & 0 deletions examples/apps/ai-collab/src/app/tasks-list/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

"use client";

import { acquirePresenceViaDataObject } from "@fluid-experimental/presence";
import type { IFluidContainer } from "@fluidframework/fluid-static";
import { type TreeView } from "@fluidframework/tree";
import {
Box,
Button,
CircularProgress,
Container,
Stack,
Tab,
Tabs,
Typography,
} from "@mui/material";
import React, { useEffect, useState } from "react";

import {
createContainer,
loadContainer,
postAttach,
containerIdFromUrl,
} from "../tinylicious";

import { buildUserPresence, type UserPresence } from "./presence";

import { TaskGroup } from "@/components/TaskGroup";
import UserProfilePhoto from "@/components/UserProfilePhoto";
import {
CONTAINER_SCHEMA,
INITIAL_APP_STATE,
SharedTreeAppState,
TREE_CONFIGURATION,
type SharedTreeTaskGroup,
} from "@/types/sharedTreeAppSchema";
import { useFluidContainerNextJs } from "@/useFluidContainerNextjs";
import { useSharedTreeRerender } from "@/useSharedTreeRerender";

// Uncomment the import line that corresponds to the server you want to use
// import { createContainer, loadContainer, postAttach, containerIdFromUrl } from "./spe";

export async function createAndInitializeContainer(): Promise<
IFluidContainer<typeof CONTAINER_SCHEMA>
> {
const container = await createContainer(CONTAINER_SCHEMA);
const treeView = container.initialObjects.appState.viewWith(TREE_CONFIGURATION);
treeView.initialize(new SharedTreeAppState(INITIAL_APP_STATE));
treeView.dispose(); // After initializing, dispose the tree view so later loading of the data can work correctly
return container;
}

// eslint-disable-next-line import/no-default-export -- NextJS uses default exports
export default function TasksListPage(): JSX.Element {
const [selectedTaskGroup, setSelectedTaskGroup] = useState<SharedTreeTaskGroup>();
const [treeView, setTreeView] = useState<TreeView<typeof SharedTreeAppState>>();
const [onlineUsers, setOnlineUsers] = useState<UserPresence>();

const { container, isFluidInitialized, data } = useFluidContainerNextJs(
containerIdFromUrl(),
createAndInitializeContainer,
postAttach,
async (id) => loadContainer(CONTAINER_SCHEMA, id),
// Get data from existing container
(fluidContainer) => {
const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION);
setTreeView(_treeView);

const presence = acquirePresenceViaDataObject(fluidContainer.initialObjects.presence);
const _onlineUsers = buildUserPresence(presence);
setOnlineUsers(_onlineUsers);

return { sharedTree: _treeView };
},
);

const taskGroups = data?.sharedTree.root.taskGroups;
useSharedTreeRerender({ sharedTreeNode: taskGroups, logId: "WorkItemRoot" });

useEffect(() => {
if (
isFluidInitialized === true &&
data !== undefined &&
data.sharedTree.root.taskGroups.length > 0
) {
setSelectedTaskGroup(data.sharedTree.root.taskGroups[0]);
}
}, [container, data, isFluidInitialized]);

return (
<Container
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
maxWidth={"lg"}
>
{onlineUsers && <UserProfilePhoto onlineUsers={onlineUsers} />}
<Typography variant="h2" sx={{ my: 3 }}>
My Work Items
</Typography>

{isFluidInitialized === false && <CircularProgress />}

{isFluidInitialized === true &&
treeView !== undefined &&
taskGroups !== undefined &&
selectedTaskGroup !== undefined && (
<React.Fragment>
<Stack direction="row" spacing={2} alignItems="center">
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={selectedTaskGroup.id}
sx={{ mb: 2 }}
aria-label="basic tabs example"
onChange={(e, newSelectedTaskGroupId) => {
const foundTaskGroup = taskGroups.find(
(taskGroup) => taskGroup.id === newSelectedTaskGroupId,
);
setSelectedTaskGroup(foundTaskGroup);
}}
>
{taskGroups?.map((taskGroup) => (
<Tab label={taskGroup.title} value={taskGroup.id} key={taskGroup.id} />
))}
</Tabs>
</Box>

<Button
variant="contained"
size="small"
color="success"
onClick={() => taskGroups.insertAtEnd(getNewTaskGroup(taskGroups.length))}
>
New Group
</Button>
</Stack>

<TaskGroup treeView={treeView} sharedTreeTaskGroup={selectedTaskGroup} />
</React.Fragment>
)}
</Container>
);
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- Too repetitive to do it
const getNewTaskGroup = (groupLength: number) => {
return {
title: `New Task Group ${groupLength}`,
description: "New task group description",
tasks: [
{
assignee: "Alice",
title: "Task #1",
description:
"This is the first task. Blah Blah blah Blah Blah blahBlah Blah blahBlah Blah blahBlah Blah blah",
priority: "low",
complexity: 1,
status: "todo",
},
{
assignee: "Bob",
title: "Task #2",
description:
"This is the second task. Blah Blah blah Blah Blah blahBlah Blah blahBlah Blah blahBlah Blah blah",
priority: "medium",
complexity: 2,
status: "in-progress",
},
{
assignee: "Charlie",
title: "Task #3",
description:
"This is the third task! Blah Blah blah Blah Blah blahBlah Blah blahBlah Blah blahBlah Blah blah",
priority: "high",
complexity: 3,
status: "done",
},
],
engineers: [
{
name: "Alice",
maxCapacity: 15,
skills: "Senior engineer capable of handling complex tasks. Versed in most languages",
},
{
name: "Bob",
maxCapacity: 12,
skills:
"Mid-level engineer capable of handling medium complexity tasks. Versed in React, Node.JS",
},
{
name: "Charlie",
maxCapacity: 7,
skills: "Junior engineer capable of handling simple tasks. Versed in Node.JS",
},
],
};
};
27 changes: 27 additions & 0 deletions examples/apps/ai-collab/src/app/tasks-list/presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import {
IPresence,
LatestMap,
type PresenceStates,
type PresenceStatesSchema,
} from "@fluid-experimental/presence";

export interface User {
photo: string;
}

const statesSchema = {
// eslint-disable-next-line @typescript-eslint/member-delimiter-style
onlineUsers: LatestMap<{ value: User }, `id-${string}`>(),
} satisfies PresenceStatesSchema;

export type UserPresence = PresenceStates<typeof statesSchema>;

export function buildUserPresence(presence: IPresence): UserPresence {
const states = presence.getStates("name:app-client-states", statesSchema);
return states;
}
Loading

0 comments on commit 178e9e3

Please sign in to comment.