Phase 3D Help System

This commit is contained in:
Nick Beckley 2026-06-04 20:48:13 +01:00
parent 0fd4c2c770
commit 78fe2dc457
24 changed files with 414 additions and 32 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/jKBSG/4rFtXEBY2x81013tFGn02ianV3tHYZDiupAhHFnqyr/fYSTqumM9mLvrGPe8/GcF16RV3tAPbo+HH5/Bp8mVKLklhAjTGAiMOFbKvua9DWpOGeMNvwfVCJ3Cy+oRy3XmmoJuAYBmBvS4KG2GhsDkhJqCGt4zql2E+pfkXKVgWenoQrTN4iTCx716C/nTfieaytX6THMpophhLWfkvI6N/NlvbrFn6832/vN6dQEUxZXzqAegdSDlo3GUnOF+SAYloJpTKGuFQNBLQwnmfMJoldjNZmn9/omJA9J96xidUUwk+hYIqOSyi0PaoLtyyF3sSC6yfFyCS+f/St6hoh6Vr7BPKW70qO72k/D7EZzNYbdzvndKnjrdnNUKRcu0SGGA8TkYIHzm/MqvnwOu13Ohz6pcQJUoi3EvfNqPP24AavmMT2EOS5kJvP0X2dQj9ScAirRQnIcl7LFeZjV6MCnX7QXhe8fis2okg1x/2FJDyf26/+B/k7pR+ehWN+gHhEKzNQdYMuowLwzCqumlliIWligWraMohKtgk/Oz2GeirVPsDtT+9DfddSPLoFOc8wz/S2y2TYxmFmn4n3BlFSkqgnJojkd5lR8BeMi6AR5iG2cz/l+kjDBFtEnN8IveFZzjOBTsVHpsfhDTY+oR1J1miqppFJCawsNAUsMUYK0BoQRYDgzreGSG8El0YKrgXKjGjrYHEElgmyyYvE1t5Z2xDJMW8Ewl4PGSoLFpG1NPZBB2hY+wC3ux6xTuiMcTI0lWIO5pApLXtfY1I1qmbBty+wFp7MhKMnUL0345wTm7KUvJ2u/zW7g+XSPlz6FMR0YQTHjtMFcSoo72bR4aLtBDqKDRufJVsFP4Q3kN4hDmFzKj8id826vxp+PvG/mFqKH8WI7d07HMAWbirMtiw/7+jr7/IoVi0W/O4//XYxxE/QTxCIbSzl/sczj8Xj8AQAA//8DAPXfSgEMBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIFntrka0MEZkcUJSTwJh/X1AzQeLcciOKrO7qquYLCfCEZCC3p9NfzxjyQmqS/QYJJjRlmjL1yM0g2SD7Rou2V0b+R2ri7/E7GUinrOXWIJWokSrHWjrKyVLn0HDGHROtKjXh80KGFwK+cfjsLTZx+Yhp8TGQgXzywcWvpTf4xs5xdU2KM+7DkiHYIuaf/e6evr09PD4cLq8WXAq58Y4MBI0drWktNVYBVaMW1GhhKUcpQaDmE44Xmg8ZU4C5WdyXn/1dzAGzHUQjZMOoMORcEwcZiuQRFnz8fioqNovuCl5v8HYcXsgzJjKI+oeZl3I3dvY3Nj49QXA3kw9+OZKanFI8YcoeNzu2CecZso+hujbZzR5DJjV5+FAdZshTTE+vPPpwGX3/B5O/A3v0Aav9HRkI4yic7JFOgmuqegcUWmmo1lJPyK3pBCc12cWQfVjjulT7kPFz2lSSgfwN84IXfbfJHn1Gm9dUpP+rS9b4zedddAVgpCaHFN1qc/XTbs4a1khWLh/WfFpz9R6dT2gzlpke03ot/xtFaLGRdmtKGHJ1gHys3sByJAMx0FsOBgyAtnbCluHEHAPNOofaaXRKuM4po5xWhlmtYOTKQcvHqSBFd0m32hZKMN1bCZI6KwRV3DI6YieogAlHZAy0lK9srR7mwgPbM4VOUoOTo8pwoEZJSZ1soRN66jox/WLJNXHOit8Ony9fY1OgnevRaU6F4i1VxnDam7ajY9ePZtQ9trZovscUcP7FqHfeprjEKVfXhaleWfd+DeV7V9vyfPWBftsiu4v2C6aqRA4+lG2+pnw+n8//AwAA//8DABpATWolBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227jNhD9FYGvFRXeRIl6S50saqRJjLXRFn0bkaOYjUwZFJXsIsi/F5KTzWVRoC+CMCTnnDlzZp5IgAOShpwfj5cPGNJIcpL8EhJMaMo0ZWrHTSNZI+tCi1JUNf+b5MRf4XfSkEpZy61BKlEjVY6VtJWdpc6h4Yw7Jko154S7kTRPBHzh8MFbLIbxD4yjHwJpyJ8+uOFxxgZf2H6YXBGHHtdhTBDsTOZmvbqiv59vdreb060Rx/lx4R1pCBrbWlNaaqwCqlotqNHCUo5SgkDNO2xPz3xIGAP0xeju3/DdkAIm24hCyIJRYchzThwkmCm3MOLu+3FmsUh0McfzJbz8Nk/kASNpRP4q5indme392WFsJ9+7s+VLcnKMwxFj8riocbvN3khwVrBCaMEYyck12L0PmK0vstt+LhFszRQ6SQ12jirDgRolJXWyhErorqpER3JyhTFg/y7rtbdxGIcuZS8iZx9wtuerywBtjzPGF+hHJDlZj9sEwUE/BLz8hnZKp1y7OM3HqyEkH6ZhGrN1SHgX4eX89f1qihFDyjaQ9tlvMO5JQwzUloMBA6Ct7bBk2DHHQLPKoXYanRKucsoop5VhVitouXJQ8rabIyQnv84abidrcRzf2Cwm6fuFQ/bSp1XvMaQPMpKGMI7CyRppJ7imqnZAoZSGai11h9yaSvBXmB3EO0ykIV3dti2zthXKcAvCgHQdk053RoiqAtXJslJC65JVYF1VCa6crNCC7CSrBcnJJg5usulzq+XSgE0c/kGbZqlIQzQry9pC2wqteA2mRsW1qrXByqgOmJK2BV7W0LacQcmsqYUFrg2o1pXdD5Uuw50PeOHHYw/f3+HWhS7kLxag5h2oT9e/RDjg4xDvb042Lm4udyQnX6cwb4RsPVvk0Qf6Tc8TfT31ye/2EcGhux4c/uwjnMclWybUoTRVazm1JRdUVV1JTVlXSxcQZSW0m9lcDPYeYzY7DHyYp+pHstttdh7t3ie0aYozv78WHrfbbNND6oZ4+LBJ3hX2WYBCSM7mzjh8OG2jhaN2rkanORWKl1QZw2ltyoq2Vd2aVtdY2pnj7ZSOU8q+ovMRbVoqfvHigrnao73/WYxXF7xYlLPZa9vTFluk/V9L7DknB4RxinhYtnXzRNYhYFyQL6bTKK7Dte97P6IdghtJU3MjCs21eWH4n/eE4kVZav38/Pz8LwAAAP//AwBl79KMGwYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUwW7cKhT9FeuujQMYY+NdNGnVUdpm1Izap7fDcN2gOHgEOG0Uzb9X2FO1WT29DULXnMu55xz8Cl4/IfRwfTq9e0afIpSQ3FrilEtCJaHiyFRf077uKsmp4B37F0pwt/gCPbTCGGYUkholEmFpQ4Z6NMRaVIwyS3kjck/9PUL/CtpVFp+dwWqOXzFEN3vo4Zvzdv6R79auMtO82CrME+59TNqbTObzfndLPl4fjneH7VTEmMGVs9ADKjMY1RiijNBEDJITJbkhDOtac5RsxGGDOZ8weD1V0T7+ud/OyWMyPa94XVHCFZxLsDrpTHnQEY8vp8xilegm18u1vG77V3jGAD0vf4u5tbsyk7t6isPiJnu1rUnHx7iuozZpDi9QwinMJwzJ4arPdYz4NEwvRx0f329n8ja++4lmSWh38+IT9MDqDkpYBZomndzsiwvH3eTQp/VjCs5HZ/6rVw0l3N0Xh0mncQ5Pb/y432Te/w+VP2nz4DwW+xvogTLktu6QjJxJIjqriW5qRaSs5YjMqJYzKGE3++T8Mi+x2PuE38M6EvTwXk8RN37XwTy4hCYtIc/5j8y5OoTZLiYVf7xktKJVTWkGLem0pOILWhfQJMxDHMNy6ff1LYRLvoJ2SwjoU3HQ6aH4oOMD9KB0Z5hWWmktjRmxoThSS7WkrUVpJVrBbWuFElYKRY0UemDC6oYNY65ACZijU6xplS1vbMs7wjvWEtF2gnQWOeENZdiOUracv9GxuJsyTpuOCrQ1UThaIhTTRIm6JrZudMvl2LZ8/EuSSx4YzQJbfN7e3cbA2g6tZIQL1hChFCOdaloytN2gBtlhYzLnWwwep7+E+uRMmOM8puKSkOKNdF8Wn/8dxZqWH86Tn6tHN7N5xFBkj7Xz+alcbD2fz+dfAAAA//8DAGxODPyCBAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTyW7bMBD9FYFnU+EmStQtcFrUSNsYidEWvY3IUU1EoQyKyoLA/15QdpGmt14IYsA385bhKwnwgKQll4fDh0cMaSIrkvxSEkxoyjRlasdNK1krm1ILIZuG/yQr4q/xhbSkVtZya5BK1EiVYxXtZG+pc2g4446JSuWe8Gsi7SsBXzp89BbLcfqGcfJjIC357oMbn/Js8KUdxtmVcRxwE6YEwWYyXzfra/r5cru72Z5eTThlcOkdaQka21lTWWqsAqo6LajRwlKOUoJAzXvsTjAfEsYAQzm5+7f5bkwBk21FKWTJqDDkuCIOEmTKHUy4ezlkFotFV7m+WsrLtX0ljxhJK1Z/zDy1u7CDv3iYutkP7uJ0Jpjus8pDHA8Yk8fFk0XmMEDyYyjOk9aDx5DIitzcFdsBUj/Gh3dG3Z30b/5D/i5P//CMdk7o1uMcEmkJV5ysyBewex+w2FyRljCOwskGaS+4pqpxQKGShmotdY/cmlpkzHoMyYd5nKdiExL+iosA0pKPMEx4on4Z7d4ntGmOWdUPnXdhG0c321S8+c9ZyUrJWAbN6TCn4hadj2gTZn27OJ/7/QMRWiyg9RwjhlRsIe2LTzDtSUsMNJaDAQOgre2xYtgzx0Cz2qF2Gp0SrnbKKKeVYVYr6LhyUPGuzxWyIpjjLpYN6/q6snVtKYO6psogp0ZzpKxhtYauRi7tOx+LmyHjwDZMoZPUYO+oMhyoUVJSJyuohe7rWvR/WXJOn7NssMPH019ZGGjnGnSaU6F4RZUxnDamqmlXN53pdIOVzZyvMQYc/jLqi7dxnMY+FeflKd5ZdzuH/N+LZZGefKDPS0ZXo73HWOSMwYe83udYj8fj8TcAAAD//wMAGEeVrDYEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUzW7bOBB+FYFnUaEoihIF7MFwWtRI0xiJu7vojSKHMRFZNCjKaRD43ReU5dopetibMCJnvr/hO+rlDlCDFvv9pwP0YUApCnYqUUI5JhwTtslFw3hDaJYLzhkVP1CK7B28oQZVTKlcCcAFcMBMkxK3hVFYaxA5yTWhJYs95fOAmnckbabhYBVkbvgb/GBdjxr0j+21e42zpc1U50adedfBqh+C7FUE8221vMNfF+vNw/p0aoAhXs6sRg0qZNFWihhcKOCYFUbiGpTBVEhWAi9pySMIaTPbB/C97LJBv1zmaxd6CKqhGS0ygqlAxxRpGWSE3MoBNm/7iGKS6DbW06k8fTbv6AAeNTQ9i3lqd6M6e7Mb2tF2+iZI/wzBeLmDV+df4CA7lKK9d3vwwcKkzcNTcoGUk4xklFNCUIrupdraHpLVbfLQRcJS1YSBLrAAozETucSCFQXWRSkryk1VUYNSdAe+h+6q671V3g3OhGSWPPkwZ+n6YPvRjUOy6gM8exlO9z7LbgCUovXYdnbYPoLUbxv3OMZ//dhFLsvRe+hDspZhm3yRwxY1SMha5VJIISVXykBJwBBNJCeVBq45aEZ1pZlgmjNBFGeyzZmWZd6aWEEpehjDfgxn/X9egdh4u9uBviDYTBqvOxmM87uVhj5YY6M15xNToLpuYpXMPZedhT58EBk1iORAdVEDNjTnmNVaYlkWAnNecAO5EhXNT4IZ+zz+0okYo1lhyrrmrWhzAtRUNSOVaYlWplBCly1UtCJVDbVQhLQly+tWKKIhL5mO7LzTowq/R6GYDHoc+7idySrSfrU9/jkFe1Zk7V1wynXRMNvB09sQYPcn15Zut3eDDZG/ma39PsC96+Pfef9nzebLCxcuNYh7kEyrJ2jBCsY1JqximDFTY1GWGjNatsKYNudFgVJ069QL+CQGTNp+8uScqYenZOHV1gZQYfRx8r8TqZOdn88rc9Ej+/Zps3QeFvt9Olf/OkSRTs3OAfjwrnwf4vGtG8IV5ZnaE3TmDOwqThoOp6fKxiLXugbNc0xZXmImRI5rUVa4repWtLyGUl3imjyCth5UmPpt/Binzcr+KZcLH6yRKgxxeb46NcVpzud85HeQV7b9un0afmXdHKW5U05iZJ9OD+eUoP/1bh6Px+N/AAAA//8DACFE8GcoBgAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTTW/cOAyG/4rBs+Xo25ZvwaSLHWTbDNpBd7E3WaIbIY48kOW0RTD/vZBnijZ724sgECL5vg+pV4j2GaGH29Pp3QvGvEANOWwhTrkmVBMqj8z0UveMNqoTujP6X6gh3ON36KGVzjFnkAjUSKSnigxidMR7NIwyT7mSpab9skD/CjY0Hl+Cw2ZePmNawhyhh79D9PPX0tuGxk3z6ps0T7iPS7bRFTEf9rt78tft4fhwuLxacCnJTfDQg7BiaB0diXCoiRSjJR26kXBjpUKtuNJFhA1NiBlTtFOz+Kdf/f2cI2bX84aLhhJu4FyDt9kWyYNd8Pj9VFRsiO5KvN7C27V/hRdM0PP6J8xLuRs3hZvnZVjD5G8uZ7bLU3F5SvMJUw64MdlsTpPNYY7VtdNuChgz1PDwqTpMNo9zen4D6tPF//5/2D+W7u++oVsz+t28xgw9MEmhhvfWPYaI1f4OeqAMuRcdkpEzTWTnLbFKGKK10CMyZ1rOoIbdHHOI67wu1T5m/JI2A9DDH3Za8CL9NrnHkNHlNRVX/2w6Dmn2q8vVL/6MNrQRtCh5WPNpzdVH9CGhy1j8HdN6rfefFK75lrRbU8KYq4PNj9WfdnmEHoztHLPGGmu1cyMqiiP11GraetReo5fct14a6bU01GlpBya9VWwYSwRqwDLuatswL5VrR8mJ4iMSKbwgHSpBqOCqVa1sFdVvOFYPU8mzrqMSvSAGR0+kYZYYKQTxQtmW67Ft+fgbkuv0GS2APb5c/sqmQHvfodeMcMkUkcYw0hnVkqHtBjPoDpUrmu8xRZx+A/U+uDQv85ir6/JUb9B9XGP579W2SF9DJN+2Gd3N7glTVWZsQyzrfR3r+Xw+/wAAAP//AwD+oJloNgQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS5Som7FO0IWbepEs0iC3ETmKCcvUgqScGIb/e0HtFo5PRS8EMeLMvHzmpV5IgEckA7k+nT48YciJ1CT7LSSY0JRpytSRm0HpgbOm7TnTSnwnNfG3+EwG0ilruTVIJWqkyrGWjnKy1Dk0nHHHRKtKTfiRyPBCwDcOn7zFZklfMSa/BDKQv31wy8/SG3xj52V1TVxm3IeUIdgi5q/97pb+eX043h3OpxKmktx4RwYiQY6dZROVFjVVcgLao52oMKBa1K1odREBvvEhYwwwN8k9vPV3Sw6Y7SAaIRtGhSGvNXGQoUgeIeHx+VRUbIhuSrzewtt2eCFPGMkg6n9hnstd2dlfPaZx9bO7Oq8Z0kPa1glsXuIzqckpLieM2ePG5zolfBzn5yOkh4/nM2WbPvxCu2Z0u2UNmQyEy47UZAM0z5D9EqqLxt3sMeTtY44+JG//q5YkNbn7Uh1myNMSH9/N48sZ8/5/UP4E9t4HrPY3ZCCMo3CyRzoJrqnqHVBopaFaSz0ht6YTnNRkt4Tsw7qsqdqHjD/idiUykI8wJzzru4723me0eY3lnt+2Zoe4uNXm6m2WnDWskYyVpDWf1lx9Rucj2ozlEse4Xup9fZ8itNiSdmuMGHJ1gHxf/QHpngzEQG85GDAA2toJW4YTcww06xxqp9Ep4TqnjHJaGWa1gpErBy0fpxIhNcFinWpz69RxJjTr6WR5R5XTnILkhgqpuetlxwwUg71xrO7mkge2ZwqdpAYnR5XhQI2SkjrZQif01HVi+g3JxQ+cFcAOn87vblOgneux9BWKt1QZw2lv2o6OXT+aUffY2qL5FmPA+TdQn7yNS1qmXF0cUr1D93kN5d9RbW756QP9tc3oZrEPGKsyY/ChPJXLWF9fX1//AQAA//8DAO7pQJCCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUXW/lJhD9KxavNV7AGIPf0iSrXm2zifZetVXfxjAkNA6+wji7qyj/vcI32U9V6otlDTDnzJkz80QiPCAZyNnxePmIMS+kJjlsIcGEokxRJg/cDFINnDWdVsIw8TepSXiHn8lAemkttwZpiwqpdKyjY+stdQ4NZ9wx0cmSE24XMjwRCI3Dx2CxmZc/MC1hjmQgf4bo5o8FG0Jjp3l1TZon3MUlQ7SFzPvd+Tv6+9nN4frmdGvBpTxugiMDaaEde8s8bS0qKlsPVKP1VBiQHapOdKqQgNCEmDFFmJrF3X/Fd3OOmO0gGtE2jApDnmviIEOhPMKCh8/HwmKT6KLE6y28/Q5P5BETGUT9KuYp3Rs7hTcPy7iGyb3ZvqQmxzQfMeWAmxrX++orCc4a1gglGCM1uQJ7FyJWu4vqeiolgtVMomupQe+oNByokW1LXdtBL5Tve+FJTd5hijh9k/Uq2DQvs8/Vi8jVdzj7s/PLCOOEBeMtTAuSmuyWfYboYJojXn5Cu+ZTrkNay/H5HHOI67wu1S5mvE3wcv76/nxNCWOubiDfVb/BckcGYkBbDgYMgLLWY8fQM8dAsd6hcgqdFK530kinpGFWSRi5dNDx0ZcIqcmvRcP9ai0uy1c2m0mmaeNQvfTpfAoY83cykoEwjsK1GqkXXFGpHVDoWkOVapVHbk0v+CvMAdItZjIQr8dxZNaOQhpuQRhonWetU94I0fcgfdv1UijVsR6s63vBpWt7tND6lmlBanKTZrfa/GOr260BN2n+B20uUpGBKNZ12sI4CiW5BqNRciW1Mtgb6YHJ1o7AOw3jyBl0zBotLHBlQI6u819Uuoy3IeJFWI4TfP4GVzeqaX+xAJp7kD9cf5vgAT/O6f79ycbN+8sDqcmHNZaNUO2KRT6GSD9tw3S1Tjkc7hKCQ3c1O/zZR1jGpdomVCjF9QgjZWpkVPrOU4Oqox57zk2vi6FJTS5me4+pKg6DEMtUfUl2va/Okr0LGW1eU+H318bjel/dTJD9nB6+2yTfFPajAI1oOSudcfh42kYbR+WcRqc4FZJ3VBrDqTZdT8dej2ZUGjtbFLte83HN1Qd0IaHNW8UvXtwwz+/Q3v8sxqsLXizKWfHa/rTFNmn/1xJ7rskDwrImfNi29fBEdjFi2pAv1tMo7uJVmKawoJ2jW8igdcubUs8Lw/++Z1SjtODPz8/P/wIAAP//AwA2Sn9NGwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIgtxaZCtDRCYHFOUkMObfF9RM4GRveyOK7O7qquILCfCEZCC3p9NfzxjyQmqS/QYJJjRlmjL1yM2g9MBZ0/a9Ml37hdTE3+NPMpBOWcutQSpRI1WOtXSUk6XOoeGMOyZaVXrC14UMLwR84/DZW2zi8hHT4mMgA/nkg4vfy2zwjZ3j6poUZ9yHJUOwhcw/+909fXt7eHw4XF4tuJTixjsyEAly7CybqLSoqZIT0B7tRIUB1aJuRasLCfCNDxlTgLlZ3LfX+S7mgNkOohGyYVQYcq6JgwyF8ggLPv48FRabRHcFrzd4Ow4v5BkTGUT9S8xLuxs7+xsbn54guJvJB78cSU1OKZ4wZY+bHNuG8wzZx1Bdh+xmjyGTmjx8qA4z5Cmmpz80+nBZff8/Nn8H9ugDVvs7MhDGUTjZI50E11T1Dii00lCtpZ6QW9MJTmqyiyH7sMZ1qfYh49e0sSQD+RvmBS/8bpM9+ow2r6lQ/7wNwx8+76IrACM1OaToVpurV7k5a1gjWbl8WPNpzdV7dD6hzVh2ekzrtf1/SoQWW9FuTQlDrg6Qj9UbWI5kIAZ6y8GAAdDWTtgynJhjoFnnUDuNTgnXOWWU08owqxWMXDlo+TgVpPAu7lZboJzknXYMqBXAqBIcqXFC0rGfhATTj1KWDLzKWj3MpQ5szxQ6SQ1OjirDgRolJXWyhU7oqevE9JskV8c5K3o7fL58jY2Bdq5HpzkVirdUGcNpb9qOjl0/mlH32NrC+R5TwPk3od55m+ISp1xdA1P9Id37NZTvXW3h+e4D/bFZdhftN0xVsRx8KGm+unw+n8//AgAA//8DAMd6qbMlBAAA

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Route("Help")]
public sealed class HelpController(IHelpService help, IHelpMarkdownRenderer markdown) : Controller
{
[HttpGet("")]
public IActionResult Index(string? q)
{
var articles = help.GetAllArticles();
return View(new HelpCentreIndexViewModel
{
Query = q,
ArticleCount = articles.Count,
Categories = help.GetCategories()
.Select(category => new HelpCategoryViewModel
{
CategoryName = category,
ArticleCount = help.GetByCategory(category).Count
})
.ToList(),
SearchResults = string.IsNullOrWhiteSpace(q) ? [] : help.Search(q),
RecentArticles = articles.OrderByDescending(x => x.RelativePath).Take(5).ToList()
});
}
[HttpGet("Category/{category}")]
public IActionResult Category(string category)
{
var articles = help.GetByCategory(category);
return View(new HelpCategoryArticlesViewModel
{
CategoryName = category,
Articles = articles
});
}
[HttpGet("Article/{key}")]
public IActionResult Article(string key)
{
var article = help.GetByContextKey(key);
if (article is null)
{
return View("ArticleNotFound", key);
}
return View(new HelpCentreArticleViewModel
{
Article = article,
RenderedHtml = markdown.Render(article.MarkdownContent),
RelatedArticles = help.GetRelatedArticles(article)
});
}
}

View File

@ -8,6 +8,9 @@ public interface IHelpService
IReadOnlyList<HelpArticle> GetAllArticles(); IReadOnlyList<HelpArticle> GetAllArticles();
HelpArticle? GetByContextKey(string contextKey); HelpArticle? GetByContextKey(string contextKey);
HelpArticle? GetByRelativePath(string relativePath); HelpArticle? GetByRelativePath(string relativePath);
IReadOnlyList<HelpArticle> GetByCategory(string category);
IReadOnlyList<string> GetCategories();
IReadOnlyList<HelpArticle> Search(string query);
IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5); IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5);
IReadOnlyList<HelpCoverageItem> GetCoverage(); IReadOnlyList<HelpCoverageItem> GetCoverage();
} }
@ -52,6 +55,44 @@ public sealed class HelpService(IWebHostEnvironment environment) : IHelpService
return GetAllArticles().FirstOrDefault(x => string.Equals(x.RelativePath, normalisedPath, StringComparison.OrdinalIgnoreCase)); return GetAllArticles().FirstOrDefault(x => string.Equals(x.RelativePath, normalisedPath, StringComparison.OrdinalIgnoreCase));
} }
public IReadOnlyList<HelpArticle> GetByCategory(string category)
{
if (string.IsNullOrWhiteSpace(category))
{
return [];
}
return GetAllArticles()
.Where(x => string.Equals(x.Category, category, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Title)
.ToList();
}
public IReadOnlyList<string> GetCategories() =>
GetAllArticles()
.Where(x => !string.IsNullOrWhiteSpace(x.Category))
.Select(x => x.Category)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x)
.ToList();
public IReadOnlyList<HelpArticle> Search(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return [];
}
var terms = query.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
return GetAllArticles()
.Select(article => new { Article = article, Score = SearchScore(article, terms) })
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.ThenBy(x => x.Article.Title)
.Select(x => x.Article)
.ToList();
}
public IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5) public IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5)
{ {
var tags = article.Tags.Select(x => x.ToLowerInvariant()).ToHashSet(); var tags = article.Tags.Select(x => x.ToLowerInvariant()).ToHashSet();
@ -196,4 +237,23 @@ public sealed class HelpService(IWebHostEnvironment environment) : IHelpService
string.IsNullOrWhiteSpace(tags) string.IsNullOrWhiteSpace(tags)
? [] ? []
: tags.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); : tags.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
private static int SearchScore(HelpArticle article, IEnumerable<string> terms)
{
var score = 0;
foreach (var term in terms)
{
if (Contains(article.Title, term)) score += 8;
if (Contains(article.Category, term)) score += 4;
if (article.Tags.Any(tag => Contains(tag, term))) score += 5;
if (Contains(article.QuickSummary, term)) score += 3;
if (Contains(article.MarkdownContent, term)) score += 1;
}
return score;
}
private static bool Contains(string? source, string value) =>
!string.IsNullOrWhiteSpace(source)
&& source.Contains(value, StringComparison.OrdinalIgnoreCase);
} }

View File

@ -9,6 +9,7 @@ public sealed class HelpDiagnosticsViewModel
public int TotalArticles => Articles.Count; public int TotalArticles => Articles.Count;
public int TotalValidArticles => Articles.Count(x => x.IsValid); public int TotalValidArticles => Articles.Count(x => x.IsValid);
public int TotalInvalidArticles => Articles.Count(x => !x.IsValid); public int TotalInvalidArticles => Articles.Count(x => !x.IsValid);
public int CoveragePercentage => TotalArticles == 0 ? 0 : (int)Math.Round(TotalValidArticles * 100.0 / TotalArticles);
} }
public sealed class HelpArticleDiagnosticViewModel public sealed class HelpArticleDiagnosticViewModel
@ -23,3 +24,32 @@ public sealed class HelpDrawerArticleViewModel
public IReadOnlyList<HelpArticle> RelatedArticles { get; set; } = []; public IReadOnlyList<HelpArticle> RelatedArticles { get; set; } = [];
public string RequestedContextKey { get; set; } = string.Empty; public string RequestedContextKey { get; set; } = string.Empty;
} }
public sealed class HelpCentreIndexViewModel
{
public string? Query { get; set; }
public IReadOnlyList<HelpCategoryViewModel> Categories { get; set; } = [];
public IReadOnlyList<HelpArticle> SearchResults { get; set; } = [];
public IReadOnlyList<HelpArticle> RecentArticles { get; set; } = [];
public int ArticleCount { get; set; }
public bool HasSearch => !string.IsNullOrWhiteSpace(Query);
}
public sealed class HelpCategoryViewModel
{
public string CategoryName { get; set; } = string.Empty;
public int ArticleCount { get; set; }
}
public sealed class HelpCategoryArticlesViewModel
{
public string CategoryName { get; set; } = string.Empty;
public IReadOnlyList<HelpArticle> Articles { get; set; } = [];
}
public sealed class HelpCentreArticleViewModel
{
public HelpArticle Article { get; set; } = new();
public string RenderedHtml { get; set; } = string.Empty;
public IReadOnlyList<HelpArticle> RelatedArticles { get; set; } = [];
}

View File

@ -0,0 +1,49 @@
@model HelpCentreArticleViewModel
@{
ViewData["Title"] = $"Help - {Model.Article.Title}";
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-action="Index">Help</a>
<a asp-action="Category" asp-route-category="@Model.Article.Category">@Model.Article.Category</a>
<span>@Model.Article.Title</span>
</nav>
<div class="page-heading">
<div>
<p class="eyebrow">@Model.Article.Category</p>
<h1>@Model.Article.Title</h1>
@if (!string.IsNullOrWhiteSpace(Model.Article.QuickSummary))
{
<p class="lead-text">@Model.Article.QuickSummary</p>
}
@if (Model.Article.Tags.Any())
{
<p class="muted">Tags: @string.Join(", ", Model.Article.Tags)</p>
}
</div>
<a class="btn btn-outline-secondary" asp-action="Index">Help Centre</a>
</div>
<section class="plain-card">
<div class="help-markdown-body">
@Html.Raw(Model.RenderedHtml)
</div>
</section>
@if (Model.RelatedArticles.Any())
{
<section class="list-section">
<h2>Related guides</h2>
<div class="asset-card-grid">
@foreach (var article in Model.RelatedArticles)
{
<article class="plain-card">
<h3><a asp-action="Article" asp-route-key="@article.ContextKey">@article.Title</a></h3>
<p>@article.QuickSummary</p>
<span class="status-pill">@article.Category</span>
</article>
}
</div>
</section>
}

View File

@ -0,0 +1,21 @@
@model string
@{
ViewData["Title"] = "Help article not found";
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-action="Index">Help</a>
<span>Article not found</span>
</nav>
<div class="page-heading">
<div>
<p class="eyebrow">Help</p>
<h1>Help article not found.</h1>
@if (!string.IsNullOrWhiteSpace(Model))
{
<p class="lead-text">No help article exists for <strong>@Model</strong>.</p>
}
</div>
<a class="btn btn-outline-secondary" asp-action="Index">Help Centre</a>
</div>

View File

@ -0,0 +1,41 @@
@model HelpCategoryArticlesViewModel
@{
ViewData["Title"] = $"Help - {Model.CategoryName}";
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-action="Index">Help</a>
<span>@Model.CategoryName</span>
</nav>
<div class="page-heading">
<div>
<p class="eyebrow">Help category</p>
<h1>@Model.CategoryName</h1>
<p class="lead-text">@Model.Articles.Count article@(Model.Articles.Count == 1 ? "" : "s")</p>
</div>
<a class="btn btn-outline-secondary" asp-action="Index">Help Centre</a>
</div>
<section class="list-section">
@if (!Model.Articles.Any())
{
<p class="muted">No help articles are available in this category yet.</p>
}
else
{
<div class="asset-card-grid">
@foreach (var article in Model.Articles)
{
<article class="plain-card">
<h2><a asp-action="Article" asp-route-key="@article.ContextKey">@article.Title</a></h2>
<p>@article.QuickSummary</p>
@if (article.Tags.Any())
{
<p class="muted">Tags: @string.Join(", ", article.Tags)</p>
}
</article>
}
</div>
}
</section>

View File

@ -0,0 +1,102 @@
@model HelpCentreIndexViewModel
@{
ViewData["Title"] = "Help";
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<span>Help</span>
</nav>
<div class="page-heading">
<div>
<p class="eyebrow">Guidance</p>
<h1>Help Centre</h1>
<p class="lead-text">Find practical guidance for planning, tracking, and shaping your story in PlotWeaver Studio.</p>
</div>
</div>
<section class="plain-card mb-4">
<form method="get" asp-action="Index" class="row g-2 align-items-end">
<div class="col-md-9">
<label class="form-label" for="q">Search help articles</label>
<input class="form-control" id="q" name="q" value="@Model.Query" placeholder="Search by title, topic, tag or phrase" />
</div>
<div class="col-md-3">
<button class="btn btn-primary w-100" type="submit">Search</button>
</div>
</form>
</section>
@if (Model.HasSearch)
{
<section class="list-section">
<h2>Search results</h2>
@if (!Model.SearchResults.Any())
{
<div class="empty-panel compact">
<p>No matching help articles were found.</p>
<p class="muted">Try a different search term.</p>
</div>
}
else
{
<div class="asset-card-grid">
@foreach (var article in Model.SearchResults)
{
<article class="plain-card">
<h3><a asp-action="Article" asp-route-key="@article.ContextKey">@article.Title</a></h3>
<p>@article.QuickSummary</p>
<span class="status-pill">Category: @article.Category</span>
</article>
}
</div>
}
</section>
}
<section class="list-section">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Browse</p>
<h2>Categories</h2>
</div>
<span class="soft-count">@Model.ArticleCount article@(Model.ArticleCount == 1 ? "" : "s")</span>
</div>
@if (!Model.Categories.Any())
{
<p class="muted">No help articles have been added yet.</p>
}
else
{
<div class="asset-card-grid">
@foreach (var category in Model.Categories)
{
<article class="plain-card">
<h3><a asp-action="Category" asp-route-category="@category.CategoryName">@category.CategoryName</a></h3>
<p class="muted">@category.ArticleCount article@(category.ArticleCount == 1 ? "" : "s")</p>
</article>
}
</div>
}
</section>
@if (Model.RecentArticles.Any())
{
<section class="list-section">
<h2>Recently added</h2>
<div class="table-responsive">
<table class="table align-middle">
<tbody>
@foreach (var article in Model.RecentArticles)
{
<tr>
<td><a asp-action="Article" asp-route-key="@article.ContextKey">@article.Title</a></td>
<td>@article.Category</td>
<td>@article.QuickSummary</td>
</tr>
}
</tbody>
</table>
</div>
</section>
}

View File

@ -12,24 +12,30 @@
</div> </div>
<section class="row g-3 mb-4"> <section class="row g-3 mb-4">
<div class="col-md-4"> <div class="col-md-3">
<div class="plain-card"> <div class="plain-card">
<p class="eyebrow">Articles found</p> <p class="eyebrow">Articles found</p>
<p class="writer-kpi">@Model.TotalArticles</p> <p class="writer-kpi">@Model.TotalArticles</p>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="plain-card"> <div class="plain-card">
<p class="eyebrow">Valid</p> <p class="eyebrow">Valid</p>
<p class="writer-kpi">@Model.TotalValidArticles</p> <p class="writer-kpi">@Model.TotalValidArticles</p>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="plain-card"> <div class="plain-card">
<p class="eyebrow">Invalid</p> <p class="eyebrow">Invalid</p>
<p class="writer-kpi">@Model.TotalInvalidArticles</p> <p class="writer-kpi">@Model.TotalInvalidArticles</p>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="plain-card">
<p class="eyebrow">Coverage</p>
<p class="writer-kpi">@Model.CoveragePercentage%</p>
</div>
</div>
</section> </section>
<section class="list-section"> <section class="list-section">
@ -70,7 +76,13 @@
<span class="badge text-bg-warning">Invalid</span> <span class="badge text-bg-warning">Invalid</span>
} }
</td> </td>
<td>@string.Join("; ", article.ValidationIssues)</td> <td>
@string.Join("; ", article.ValidationIssues)
@if (!article.IsValid)
{
<a class="ms-2" asp-action="Article" asp-route-path="@article.RelativePath">View invalid article</a>
}
</td>
</tr> </tr>
} }
</tbody> </tbody>

View File

@ -20,13 +20,16 @@ else
{ {
<p class="lead-text">@Model.Article.QuickSummary</p> <p class="lead-text">@Model.Article.QuickSummary</p>
} }
<p>
<a asp-controller="Help" asp-action="Article" asp-route-key="@Model.Article.ContextKey">Open in Help Centre</a>
</p>
<div class="help-markdown-body"> <div class="help-markdown-body">
@Html.Raw(Model.RenderedHtml) @Html.Raw(Model.RenderedHtml)
</div> </div>
@if (Model.RelatedArticles.Any()) @if (Model.RelatedArticles.Any())
{ {
<section class="help-related-section"> <section class="help-related-section">
<h3>Related articles</h3> <h3>Related guides</h3>
<ul> <ul>
@foreach (var related in Model.RelatedArticles) @foreach (var related in Model.RelatedArticles)
{ {

View File

@ -46,6 +46,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="RelationshipMap" asp-action="Index">Relationship Map</a> <a class="nav-link text-dark" asp-area="" asp-controller="RelationshipMap" asp-action="Index">Relationship Map</a>
</li> </li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Help" asp-action="Index">Help</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">About</a> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">About</a>
</li> </li>

View File

@ -1590,6 +1590,10 @@ a:focus-visible {
text-align: left; text-align: left;
} }
.help-markdown-body .help-markdown-table {
margin-bottom: 0;
}
.help-resizing { .help-resizing {
cursor: col-resize; cursor: col-resize;
user-select: none; user-select: none;