Skip to content

Commit 71a3e3c

Browse files
CopilotTechQuery
andauthored
[add] Prototype Generator model & component (#89)
Co-authored-by: TechQuery <[email protected]>
1 parent 728e6f5 commit 71a3e3c

File tree

22 files changed

+1423
-1076
lines changed

22 files changed

+1423
-1076
lines changed

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NEXT_PUBLIC_API_HOST = http://localhost:8080

components/Project/EvaluationDisplay.tsx

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
1-
import { RequirementEvaluation, UserRole } from '@idea2app/data-server';
1+
import {
2+
PrototypeType,
3+
PrototypeVersion,
4+
RequirementEvaluation,
5+
UserRole,
6+
} from '@idea2app/data-server';
27
import { Box, Typography } from '@mui/material';
38
import { observer } from 'mobx-react';
49
import { FC, useContext } from 'react';
510

6-
import { I18nContext } from '../../models/Translation';
11+
import { i18n, I18nContext } from '../../models/Translation';
712
import userStore from '../../models/User';
13+
import { PrototypeGenerator, PrototypeGeneratorProps } from './PrototypeGenerator';
814

9-
export const EvaluationDisplay: FC<RequirementEvaluation> = observer(
15+
export const DevelopmentScopeName = ({ t }: typeof i18n) => [
16+
t('product_prototype'),
17+
t('ui_design'),
18+
t('desktop'),
19+
t('mobile'),
20+
t('server'),
21+
];
22+
23+
export interface EvaluationDisplayProps
24+
extends RequirementEvaluation,
25+
Pick<PrototypeGeneratorProps, 'projectId' | 'messageId'> {
26+
prototypes?: PrototypeVersion[];
27+
}
28+
29+
export const EvaluationDisplay: FC<EvaluationDisplayProps> = observer(
1030
({
1131
title,
1232
scopes = [],
33+
models,
1334
developerCount,
1435
designerCount,
1536
workload,
1637
monthPeriod,
1738
budget,
1839
factor,
40+
projectId,
41+
messageId,
42+
prototypes,
1943
}) => {
20-
const { t } = useContext(I18nContext),
44+
const i18n = useContext(I18nContext);
45+
const { t } = i18n,
2146
{ roles } = userStore.session || {};
2247

2348
return (
2449
<Box
50+
className="prose"
2551
sx={{
2652
'& .evaluation-item': {
2753
marginBottom: 1,
@@ -53,9 +79,41 @@ export const EvaluationDisplay: FC<RequirementEvaluation> = observer(
5379
{t('development_scopes')}
5480
</Typography>
5581
<Box component="ul" sx={{ mt: 0.5 }}>
56-
{scopes.map(scope => (
57-
<Box key={scope} component="li" sx={{ ml: 1 }}>
58-
{scope}
82+
{scopes.map(scope => {
83+
const prototypeType = (
84+
scope === 2 ? 'desktop' : scope === 3 ? 'mobile' : undefined
85+
) as PrototypeType;
86+
87+
return (
88+
<Box
89+
key={scope}
90+
component="li"
91+
sx={{ ml: 1, display: 'flex', alignItems: 'center', gap: 1 }}
92+
>
93+
{DevelopmentScopeName(i18n)[scope]}
94+
95+
{prototypeType && (
96+
<PrototypeGenerator
97+
{...{ projectId, messageId }}
98+
type={prototypeType}
99+
prototype={prototypes?.find(({ type }) => type === prototypeType)}
100+
/>
101+
)}
102+
</Box>
103+
);
104+
})}
105+
</Box>
106+
</Box>
107+
)}
108+
{models?.[0] && (
109+
<Box className="evaluation-item">
110+
<Typography component="h4" sx={{ fontWeight: 600 }}>
111+
{t('feature_modules')}
112+
</Typography>
113+
<Box component="ol" sx={{ mt: 0.5 }}>
114+
{models.map((model, index) => (
115+
<Box key={index} component="li" sx={{ ml: 1 }}>
116+
{model}
59117
</Box>
60118
))}
61119
</Box>

components/Project/NewCard.tsx

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,36 @@ import Link from 'next/link';
55
import { FC, useContext } from 'react';
66

77
import { I18nContext } from '../../models/Translation';
8+
import type zhCN from '../../translation/zh-CN';
89

9-
export const ProjectCard: FC<Project> = observer(({ id, name, projectStatus }) => {
10+
const statusTextKeys: (keyof typeof zhCN)[] = [
11+
'project_open', // Open
12+
'project_evaluated', // Evaluated
13+
'project_contract_generated', // ContractGenerated
14+
'project_in_development', // InDevelopment
15+
'project_in_testing', // InTesting
16+
'project_maintenance', // Maintenance
17+
];
18+
19+
const bgColors: string[] = [
20+
'grey.200', // Open
21+
'success.light', // Evaluated
22+
'warning.light', // ContractGenerated
23+
'info.light', // InDevelopment
24+
'secondary.light', // InTesting
25+
'primary.light', // Maintenance
26+
];
27+
28+
const textColors: string[] = [
29+
'text.primary', // Open
30+
'success.contrastText', // Evaluated
31+
'warning.contrastText', // ContractGenerated
32+
'info.contrastText', // InDevelopment
33+
'secondary.contrastText', // InTesting
34+
'primary.contrastText', // Maintenance
35+
];
36+
37+
export const ProjectCard: FC<Project> = observer(({ id, name, status = 0 }) => {
1038
const { t } = useContext(I18nContext);
1139

1240
return (
@@ -21,25 +49,11 @@ export const ProjectCard: FC<Project> = observer(({ id, name, projectStatus }) =
2149
px: 1,
2250
py: 0.5,
2351
borderRadius: 1,
24-
bgcolor:
25-
projectStatus === '1'
26-
? 'success.light'
27-
: projectStatus === '0'
28-
? 'grey.200'
29-
: 'warning.light',
30-
color:
31-
projectStatus === '1'
32-
? 'success.contrastText'
33-
: projectStatus === '0'
34-
? 'text.primary'
35-
: 'warning.contrastText',
52+
bgcolor: bgColors[status] ?? 'grey.200',
53+
color: textColors[status] ?? 'text.primary',
3654
}}
3755
>
38-
{projectStatus === '1'
39-
? t('project_open')
40-
: projectStatus === '0'
41-
? t('project_closed')
42-
: t('project_pending')}
56+
{t((statusTextKeys[status] ?? 'project_open') as keyof typeof zhCN)}
4357
</Typography>
4458
</CardContent>
4559
<CardActions>
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { PrototypeType, PrototypeVersion } from '@idea2app/data-server';
2+
import { Box, Button, CircularProgress, Link, Typography } from '@mui/material';
3+
import { observable } from 'mobx';
4+
import { observer } from 'mobx-react';
5+
import { ObservedComponent } from 'mobx-react-helper';
6+
import { createRef } from 'react';
7+
import { inViewport, sleep } from 'web-utility';
8+
9+
import { PrototypeVersionModel } from '../../models/PrototypeVersion';
10+
import { i18n, I18nContext } from '../../models/Translation';
11+
12+
export interface PrototypeGeneratorProps {
13+
projectId: number;
14+
messageId: number;
15+
type: PrototypeType;
16+
prototype?: PrototypeVersion;
17+
}
18+
19+
@observer
20+
export class PrototypeGenerator extends ObservedComponent<PrototypeGeneratorProps, typeof i18n> {
21+
static contextType = I18nContext;
22+
23+
versionStore = new PrototypeVersionModel(this.props.projectId, this.props.type);
24+
25+
@observable
26+
accessor version = this.props.prototype;
27+
28+
private root = createRef<HTMLElement>();
29+
30+
componentDidMount() {
31+
super.componentDidMount();
32+
33+
this.pollStatusCheck();
34+
}
35+
36+
async pollStatusCheck() {
37+
const { props, version } = this,
38+
rootElement = this.root.current;
39+
40+
while (version?.status === 'pending' || version?.status === 'processing') {
41+
if (!rootElement?.isConnected) break;
42+
43+
if (inViewport(rootElement))
44+
this.version = await this.versionStore.getOne(props.prototype!.id);
45+
46+
await sleep(3);
47+
}
48+
}
49+
50+
handleGenerateClick = async () => {
51+
this.version = await this.versionStore.updateOne({
52+
evaluationMessage: this.props.messageId,
53+
});
54+
55+
return this.pollStatusCheck();
56+
};
57+
58+
renderPending() {
59+
const { t } = this.observedContext;
60+
const loading = this.versionStore.uploading > 0;
61+
62+
return (
63+
<Button
64+
variant="contained"
65+
color="primary"
66+
size="small"
67+
disabled={loading}
68+
sx={{ textTransform: 'none' }}
69+
onClick={this.handleGenerateClick}
70+
>
71+
{loading ? t('generating') : t('generate_prototype')}
72+
</Button>
73+
);
74+
}
75+
76+
renderGenerating() {
77+
const { t } = this.observedContext;
78+
79+
return (
80+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
81+
<CircularProgress size={16} />
82+
<Typography variant="body2">{t('prototype_generating')}</Typography>
83+
</Box>
84+
);
85+
}
86+
87+
renderCompleted() {
88+
const { t } = this.observedContext;
89+
const { previewLink, gitLogsLink } = this.version || {};
90+
91+
return (
92+
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
93+
{previewLink && (
94+
<Link
95+
href={previewLink}
96+
target="_blank"
97+
rel="noopener noreferrer"
98+
sx={{
99+
textDecoration: 'none',
100+
fontSize: '0.875rem',
101+
fontWeight: 500,
102+
color: 'primary.main',
103+
}}
104+
>
105+
{t('view_preview')}
106+
</Link>
107+
)}
108+
{gitLogsLink && (
109+
<Link
110+
href={gitLogsLink}
111+
target="_blank"
112+
rel="noopener noreferrer"
113+
sx={{
114+
textDecoration: 'none',
115+
fontSize: '0.875rem',
116+
fontWeight: 500,
117+
color: 'text.secondary',
118+
}}
119+
>
120+
{t('view_ai_log')}
121+
</Link>
122+
)}
123+
</Box>
124+
);
125+
}
126+
127+
renderFailed() {
128+
const { t } = this.observedContext;
129+
const { errorMessage, gitLogsLink } = this.version || {};
130+
131+
return (
132+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
133+
<Typography variant="body2" color="error" sx={{ fontSize: '0.875rem' }}>
134+
{errorMessage || t('prototype_generation_failed')}
135+
</Typography>
136+
{gitLogsLink && (
137+
<Link
138+
href={gitLogsLink}
139+
target="_blank"
140+
rel="noopener noreferrer"
141+
sx={{
142+
textDecoration: 'none',
143+
fontSize: '0.875rem',
144+
fontWeight: 500,
145+
color: 'text.secondary',
146+
}}
147+
>
148+
{t('view_ai_log')}
149+
</Link>
150+
)}
151+
</Box>
152+
);
153+
}
154+
155+
render() {
156+
const { version } = this;
157+
158+
return (
159+
<Box ref={this.root} sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
160+
{!version || version.status === 'pending'
161+
? this.renderPending()
162+
: version.status === 'processing'
163+
? this.renderGenerating()
164+
: version.status === 'completed'
165+
? this.renderCompleted()
166+
: this.renderFailed()}
167+
</Box>
168+
);
169+
}
170+
}
File renamed without changes.

components/Project/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC } from 'react';
22

33
import { Project } from '../../models/Project';
4-
import { ProjectCard } from './Card';
4+
import { ProjectCard } from './PublicCard';
55

66
export interface ProjectListLayoutProps {
77
defaultData: Project[];

components/ScrollBoundary.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,25 @@ export type ScrollBoundaryProps = PropsWithChildren<
1111
}
1212
>;
1313

14-
function touch(edge: EdgePosition, onTouch: TouchHandler) {
15-
return (node: HTMLElement | null) => {
16-
if (node) {
17-
new IntersectionObserver(([{ isIntersecting }]) => {
18-
if (isIntersecting) {
19-
onTouch(edge);
20-
}
21-
}).observe(node);
22-
}
23-
};
24-
}
14+
const EdgeOrder: EdgePosition[] = ['top', 'right', 'bottom', 'left'];
15+
16+
const touch = (edge: EdgePosition, onTouch: TouchHandler) => (node: HTMLElement | null) => {
17+
if (!node) return;
18+
19+
const anchor = node.parentElement?.parentElement;
20+
21+
const { overflowX, overflowY } = anchor ? getComputedStyle(anchor) : {};
22+
23+
const root = `${overflowX}${overflowY}`.match(/auto|scroll/) ? anchor : null;
24+
25+
const edgeMargins = Array(4).fill('0px');
26+
edgeMargins[EdgeOrder.indexOf(edge)] = '200px';
27+
28+
new IntersectionObserver(([{ isIntersecting }]) => isIntersecting && onTouch(edge), {
29+
root,
30+
rootMargin: edgeMargins.join(' '),
31+
}).observe(node);
32+
};
2533

2634
export const ScrollBoundary: FC<ScrollBoundaryProps> = ({
2735
className = '',
@@ -30,7 +38,7 @@ export const ScrollBoundary: FC<ScrollBoundaryProps> = ({
3038
left,
3139
right,
3240
bottom,
33-
children
41+
children,
3442
}) => (
3543
<div className={className} style={{ position: 'relative' }}>
3644
<div

0 commit comments

Comments
 (0)