feat(client): Implement AiScript scratchpad
This commit is contained in:
		
							parent
							
								
									df69ca4d56
								
							
						
					
					
						commit
						b5981ab544
					
				|  | @ -476,6 +476,9 @@ state: "状態" | |||
| sort: "ソート" | ||||
| ascendingOrder: "昇順" | ||||
| descendingOrder: "降順" | ||||
| scratchpad: "スクラッチパッド" | ||||
| scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。" | ||||
| output: "出力" | ||||
| 
 | ||||
| _theme: | ||||
|   explore: "テーマを探す" | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ | |||
| 		"@koa/cors": "3.0.0", | ||||
| 		"@koa/multer": "2.0.2", | ||||
| 		"@koa/router": "8.0.8", | ||||
| 		"@syuilo/aiscript": "0.0.0", | ||||
| 		"@syuilo/aiscript": "0.0.2", | ||||
| 		"@types/bcryptjs": "2.4.2", | ||||
| 		"@types/bull": "3.12.1", | ||||
| 		"@types/cbor": "5.0.0", | ||||
|  | @ -250,6 +250,7 @@ | |||
| 		"vue-marquee-text-component": "1.1.1", | ||||
| 		"vue-meta": "2.3.3", | ||||
| 		"vue-prism-component": "1.1.1", | ||||
| 		"vue-prism-editor": "0.5.1", | ||||
| 		"vue-router": "3.1.6", | ||||
| 		"vue-style-loader": "4.1.2", | ||||
| 		"vue-svg-inline-loader": "1.5.0", | ||||
|  |  | |||
|  | @ -156,7 +156,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faTerminal, faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { ResizeObserver } from '@juggle/resize-observer'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | @ -470,6 +470,11 @@ export default Vue.extend({ | |||
| 					to: '/games', | ||||
| 					icon: faGamepad, | ||||
| 				}, null] : []), { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('scratchpad'), | ||||
| 					to: '/scratchpad', | ||||
| 					icon: faTerminal, | ||||
| 				}, null, { | ||||
| 					type: 'link', | ||||
| 					text: this.$t('help'), | ||||
| 					to: '/docs', | ||||
|  |  | |||
|  | @ -0,0 +1,154 @@ | |||
| <template> | ||||
| <div class=""> | ||||
| 	<portal to="icon"><fa :icon="faTerminal"/></portal> | ||||
| 	<portal to="title">{{ $t('scratchpad') }}</portal> | ||||
| 
 | ||||
| 	<div class="_panel"> | ||||
| 		<prism-editor v-model="code" :line-numbers="false" language="js"/> | ||||
| 		<mk-button style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><fa :icon="faPlay"/></mk-button> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<mk-container :body-togglable="true"> | ||||
| 		<template #header><fa fixed-width/>{{ $t('output') }}</template> | ||||
| 		<div class="bepmlvbi"> | ||||
| 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | ||||
| 		</div> | ||||
| 	</mk-container> | ||||
| 
 | ||||
| 	<section class="_card" style="margin-top: var(--margin);"> | ||||
| 		<div class="_content">{{ $t('scratchpadDescription') }}</div> | ||||
| 	</section> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import "prismjs"; | ||||
| import "prismjs/themes/prism.css"; | ||||
| import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons'; | ||||
| import PrismEditor from 'vue-prism-editor'; | ||||
| import { AiScript, parse, utils, values } from '@syuilo/aiscript'; | ||||
| import i18n from '../i18n'; | ||||
| import MkContainer from '../components/ui/container.vue'; | ||||
| import MkButton from '../components/ui/button.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n, | ||||
| 
 | ||||
| 	metaInfo() { | ||||
| 		return { | ||||
| 			title: this.$t('scratchpad') as string | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		MkButton, | ||||
| 		PrismEditor, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			code: '', | ||||
| 			logs: [], | ||||
| 			faTerminal, faPlay | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		code() { | ||||
| 			localStorage.setItem('scratchpad', this.code); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		const saved = localStorage.getItem('scratchpad'); | ||||
| 		if (saved) { | ||||
| 			this.code = saved; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async run() { | ||||
| 			this.logs = []; | ||||
| 			const aiscript = new AiScript({ | ||||
| 				dialog: values.FN_NATIVE(async ([title, text, type]) => { | ||||
| 					await this.$root.dialog({ | ||||
| 						type: type ? type.value : 'info', | ||||
| 						title: title.value, | ||||
| 						text: text.value, | ||||
| 					}); | ||||
| 				}), | ||||
| 				confirm: values.FN_NATIVE(async ([title, text]) => { | ||||
| 					const confirm = await this.$root.dialog({ | ||||
| 						type: 'warning', | ||||
| 						showCancelButton: true, | ||||
| 						title: title.value, | ||||
| 						text: text.value, | ||||
| 					}); | ||||
| 					return confirm.canceled ? values.FALSE : values.TRUE | ||||
| 				}), | ||||
| 			}, { | ||||
| 				in: (q) => { | ||||
| 					return new Promise(ok => { | ||||
| 						this.$root.dialog({ | ||||
| 							title: q, | ||||
| 							input: {} | ||||
| 						}).then(({ canceled, result: a }) => { | ||||
| 							ok(a); | ||||
| 						}); | ||||
| 					}); | ||||
| 				}, | ||||
| 				out: (value) => { | ||||
| 					this.logs.push({ | ||||
| 						id: Math.random(), | ||||
| 						text: value.type === 'str' ? value.value : utils.valToString(value), | ||||
| 						print: true | ||||
| 					}); | ||||
| 				}, | ||||
| 				log: (type, params) => { | ||||
| 					switch (type) { | ||||
| 						case 'end': this.logs.push({ | ||||
| 							id: Math.random(), | ||||
| 							text: utils.valToString(params.val, true), | ||||
| 							print: false | ||||
| 						}); break; | ||||
| 						default: break; | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			let ast; | ||||
| 			try { | ||||
| 				ast = parse(this.code); | ||||
| 			} catch (e) { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'error', | ||||
| 					text: 'Syntax error :(' | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 			try { | ||||
| 				await aiscript.exec(ast); | ||||
| 			} catch (e) { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .bepmlvbi { | ||||
| 	padding: 16px; | ||||
| 
 | ||||
| 	> .log { | ||||
| 		&:not(.print) { | ||||
| 			opacity: 0.7; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -48,6 +48,7 @@ export const router = new VueRouter({ | |||
| 		{ path: '/my/antennas', component: page('my-antennas/index') }, | ||||
| 		{ path: '/my/apps', component: page('apps') }, | ||||
| 		{ path: '/preferences', component: page('preferences/index') }, | ||||
| 		{ path: '/scratchpad', component: page('scratchpad') }, | ||||
| 		{ path: '/instance', component: page('instance/index') }, | ||||
| 		{ path: '/instance/emojis', component: page('instance/emojis') }, | ||||
| 		{ path: '/instance/users', component: page('instance/users') }, | ||||
|  |  | |||
|  | @ -80,6 +80,7 @@ export default { | |||
| 				el._keyHandler = (e: KeyboardEvent) => { | ||||
| 					const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; | ||||
| 					if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; | ||||
| 					if (document.activeElement && document.activeElement.attributes['contenteditable']) return; | ||||
| 
 | ||||
| 					for (const action of actions) { | ||||
| 						const matched = match(e, action.patterns); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue